Compare commits

...

80 Commits

Author SHA1 Message Date
bash
0c9131692c refactor(Core/Movement): Align MoveFarTo preamble + drop spline-plan throttle 2026-05-30 21:28:48 +02:00
bash
1601d6a514 refactor(Core/Movement): Drop IsWaitingForLastMove throttle 2026-05-30 21:07:36 +02:00
bash
dd05767dcc fix(Core/Movement): Bypass stale lastMove gate when bot stopped + loosen probe short-circuit 2026-05-30 20:56:05 +02:00
bash
7278a3bfcb refactor(Conf): Hardcode master-walk-pace distance to 5y, drop config 2026-05-30 20:29:01 +02:00
bash
ecbf3fdec2 fix(Conf): Add missing AiPlayerbot.WalkDistance to playerbots.conf.dist 2026-05-30 20:26:05 +02:00
bash
01ea88624a fix(Core/Travel): Batch NODE_PREPATH into the walk-spline dispatch so per-tick re-resolve actually moves the bot 2026-05-30 20:22:27 +02:00
bash
02844dffd4 fix(Core/RPG): Drop per-tick travelplan whisper to silence spam 2026-05-30 20:18:59 +02:00
bash
2597880d38 fix(Core/Travel): Pass GAMEOBJECT_TYPE_SPELLCASTER to GetGameObjectIfCanInteractWith 2026-05-30 20:07:51 +02:00
bash
f4d308b684 refactor(Core/Travel): Remove dead spline-progress tracking and unused NODE_TELEPORT path 2026-05-30 19:56:41 +02:00
bash
a0e21d9f38 feat(Core/Travel): Re-enable area-trigger, static-portal, and teleport-spell nodes 2026-05-30 19:34:04 +02:00
bash
ed9e7227fb feat(Core/Travel): K-nearest node search, cropPathTo reuse, cross-map pathToEnd 2026-05-30 19:20:25 +02:00
bash
72d9ecabb9 fix(Core/Travel): mmap-path startPath and endPath in GetFullPath 2026-05-30 19:05:00 +02:00
bash
d9a8ac3a2a feat(Core/Travel): Exclude area-trigger, static-portal, teleport-spell path types from PR 2026-05-30 18:57:42 +02:00
bash
8cb54416bf fix(Core/RPG): Per-tick re-resolve travel plan instead of advancing cached plan 2026-05-30 18:38:22 +02:00
bash
558e9ee1e1 feat(Core/Travel): Handle NODE_TELEPORT (hearthstone) and NODE_AREA_TRIGGER 2026-05-30 18:27:52 +02:00
bash
563a415532 fix(Core/Movement): ChaseTo tries mmap path before MoveChase 2026-05-30 18:19:54 +02:00
bash
126294cc38 fix(Core/RPG): Use GetNearPoint and followAngle in MoveWorldObjectTo, bump travel-node threshold to sightDistance 2026-05-30 18:11:06 +02:00
bash
3b106260ac fix(Core/Travel): Soft-bias STEEP at regen PathGenerator sites 2026-05-30 18:07:02 +02:00
bash
b3a8d9f4be Revert "fix(Core/RPG): Drop chained probe and waypoint dispatch in MoveFarTo"
This reverts commit 3384fa4fcfdc8e394653f4604f7de97cf7da9571.
2026-05-30 18:05:39 +02:00
bash
3384fa4fcf fix(Core/RPG): Drop chained probe and waypoint dispatch in MoveFarTo 2026-05-30 15:44:43 +02:00
bash
0d3d38b007 fix(Core/RPG): Align MoveFarTo, MoveWorldObjectTo, MoveRandomNear with cmangos 2026-05-30 15:35:57 +02:00
bash
4e8e3e2afe fix(Core/RPG): Scope do-quest yield-to-grind to current objective only 2026-05-30 15:04:10 +02:00
bash
8c027e3a70 fix(Core/RPG): Drop over-strict MoveFarTo and MoveWorldObjectTo guards 2026-05-30 14:54:36 +02:00
bash
896ad3bf75 fix(Core/RPG): Require LOS from candidate to GO in MoveWorldObjectTo 2026-05-30 14:37:38 +02:00
bash
bdefd38830 fix(Core/Loot): Drop hostiles-in-sight gate on loot-available trigger 2026-05-30 14:28:03 +02:00
bash
5f61fe9ddf refactor(Core/Movement): Drop redundant bot filter setters at PathGenerator sites 2026-05-30 13:53:18 +02:00
bash
82ebaa9594 refactor(Core/Movement): Rename SetAreaCost calls to SetNavTerrainCost 2026-05-30 13:53:18 +02:00
bash
51cea4d76c fix(Core/Movement): Apply bot filter setters at all PathGenerator construction sites 2026-05-30 13:53:18 +02:00
bash
aae47b06c7 fix(Core/Travel): Apply NAV_WATER cost bias on regen PathGenerator 2026-05-30 13:53:18 +02:00
bash
d72d3ded6c fix(Core/Travel): Exclude NAV_GROUND_STEEP on regen PathGenerator 2026-05-30 13:53:17 +02:00
bash
974faf0cb0 fix(Core/Travel): Hoist portal/transport cheat above 2-point reject 2026-05-30 13:53:17 +02:00
bash
e052ec3b17 fix(Core/Travel): Match cmangos buildPath stitching, drop 75y guard 2026-05-30 13:53:17 +02:00
bash
4a991c194d fix(Core/Travel): Preserve walk paths from taxi-path overwrite 2026-05-30 13:53:17 +02:00
bash
ed31f8f8a7 chore(Core/Travel): Warn admins to shutdown after generatenode 2026-05-30 13:53:17 +02:00
bash
479794b66b fix(Core/Travel): Skip 5y dedup when loading nodes from DB 2026-05-30 13:53:17 +02:00
bash
337fbca8c0 chore(DB/Travel): Temporarily disable Aldrassil ramp anchors 2026-05-30 13:53:17 +02:00
bash
fe12f1a708 fix(Core/Travel): Drop 2-point check, keep last-segment teleport guard 2026-05-30 13:53:17 +02:00
bash
8916cf83c0 fix(Core/Travel): Reject paths with >75y final-segment teleport jumps 2026-05-30 13:53:17 +02:00
bash
77caf85fd1 fix(Core/Travel): Reject 2-point BuildShortcut paths between non-adjacent nodes 2026-05-30 13:53:17 +02:00
bash
5e5d41f878 chore(Core/Travel): Bump 2-point shortcut threshold to 75y 2026-05-30 13:53:17 +02:00
bash
63c5d674d6 fix(Core/Travel): Reject 2-point BuildShortcut teleports in chained probe 2026-05-30 13:53:17 +02:00
bash
43ee732003 Revert non-progress chained-probe detection (broke valid paths) 2026-05-30 13:53:17 +02:00
bash
f42f37399f fix(Core/Travel): Loosen chained-probe non-progress threshold 2026-05-30 13:53:17 +02:00
bash
c7929482c4 fix(Core/Travel): Bail chained probe on non-progress oscillation 2026-05-30 13:53:17 +02:00
bash
e1489f213e fix(Core/Travel): Chunk all saveNodeStore phases (deletes, nodes, links) 2026-05-30 13:53:17 +02:00
bash
007189fd5c fix(Core/Travel): Chunk saveNodeStore path inserts to avoid mega-tx 2026-05-30 13:53:17 +02:00
bash
eabefb1d33 feat(DB/Travel): Add Aldrassil ramp travelnode anchors 2026-05-30 13:53:17 +02:00
bash
2208c80caa chore(Core/Debug): Compact debug-move whisper format 2026-05-30 13:53:17 +02:00
bash
a472fc2d68 feat(Core/Travel): Sparse-segment clip in LaunchWalkSpline 2026-05-30 13:53:17 +02:00
bash
bac63e2a8c feat(Core/RPG): Prefix-trim and sparse-segment clip on path dispatch 2026-05-30 13:53:17 +02:00
bash
690288b5cc feat(Core/RPG): Port cmangos 8-angle LOS+navmesh-snap to MoveWorldObjectTo 2026-05-30 13:53:17 +02:00
bash
57134918cb chore(Core/RPG): Loosen Z-mismatch threshold from 5y to 10y 2026-05-30 13:53:17 +02:00
bash
324b50f1be fix(Core/RPG): Reject mmap paths whose endpoint Z misses dest 2026-05-30 13:53:17 +02:00
bash
7d8d8c6b31 fix(Core/RPG): Reject mmap paths that LOS-fail any segment 2026-05-30 13:53:17 +02:00
bash
34b0432aaa feat(Core/RPG): Switch POI when current cluster is empty 2026-05-30 13:53:17 +02:00
bash
edc999c8ac fix(Core/RPG): Stop next to quest objects instead of on top of them 2026-05-30 13:53:17 +02:00
bash
85e2a940a1 chore: Drop bot movement console logs 2026-05-30 13:53:17 +02:00
bash
6754a95890 chore: Tighten comments in travel and movement code 2026-05-30 13:53:17 +02:00
bash
d0fac16c85 chore(Core/Travel): Drop cmangos reference in RefineWalkPoints comment 2026-05-30 13:53:17 +02:00
bash
a64c721f35 fix(Core/RPG): LOS check on MoveRandomNear samples to avoid tree tunneling 2026-05-30 13:53:17 +02:00
bash
27503a9c37 Revert "fix(Core/Travel): LOS check before trusting raw cmangos waypoints" 2026-05-30 13:53:17 +02:00
bash
1a7e6db0c9 fix(Core/Travel): LOS gate on empty-probe single-waypoint fallback 2026-05-30 13:53:16 +02:00
bash
6ae973bb8e fix(Core/Travel): LOS check before trusting raw cmangos waypoints 2026-05-30 13:53:16 +02:00
bash
101da6ecd3 chore(Core/Travel): Revert travelnode threshold to 50y 2026-05-30 13:53:16 +02:00
bash
605e7586c5 chore(Core/Travel): Bump travelnode threshold to 75y 2026-05-30 13:53:16 +02:00
bash
129cb252cf fix(Core/Travel): Trust travelnode waypoints when AC mmap rejects segments 2026-05-30 13:53:16 +02:00
bash
088537277c feat(Core/Travel): Hardcode 50y travelnode threshold 2026-05-30 13:53:16 +02:00
bash
6944da8d69 core filter isnt working yet 2026-05-30 13:53:16 +02:00
bash
980c1b8cd8 refactor(Core/Travel): Drop redundant NAV_GROUND_STEEP excludes (core handles via IsBot) 2026-05-30 13:53:16 +02:00
bash
ad14420400 fix(Core/Travel): Exclude NAV_GROUND_STEEP at all bot PathGenerator sites 2026-05-30 13:53:16 +02:00
bash
1d0aeec7b9 feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos 2026-05-30 13:53:16 +02:00
bash
7741626631 feat(Core/Travel): Cap bots at 50° via NAV_GROUND_STEEP exclude 2026-05-30 13:53:16 +02:00
bash
806013a4c9 feat(Core/Debug): Trace movement entry points and visualize travel nodes 2026-05-30 13:53:16 +02:00
bash
3269d1a4b3 feat(Core/RPG): MoveFarTo flow, quest-pursuit at POI, MoveRandomNear retries 2026-05-30 13:53:16 +02:00
bash
1ae72b0888 feat(Core/Travel): Travel-node graph routing for long-distance pathing 2026-05-30 13:53:16 +02:00
bash
0a9bf70305 feat(Core/Loot): Quest GO loot, bag-make-room, item-pursuit 2026-05-30 13:53:16 +02:00
bash
e18fdd02cd chore(Tools): Add mmap/vmap client-data extraction script 2026-05-30 13:53:16 +02:00
bash
a23158ef52 feat(DB/Travel): Import cmangos travel-node graph 2026-05-30 13:53:16 +02:00
dillyns
4a63ee37e2
Shadow Priest Vampiric Embrac (#2410)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

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

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

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

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

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

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



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


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



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



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



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



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

## Final Checklist

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

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

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

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

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

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

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

The exact priority list is:

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

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

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

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

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

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

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

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

## Feature Evaluation

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

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

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

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

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

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

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

## How to Test the Changes

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

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

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

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

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

## Impact Assessment

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

Discussed above in processing costs.

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

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

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

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

## AI Assistance

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

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

## Final Checklist

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

## Notes for Reviewers

---------

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

51
.claude/settings.json Normal file
View File

@ -0,0 +1,51 @@
{
"permissions": {
"allow": [
"Bash(awk '-F[\\(\\),]' ' *)",
"Bash(xargs ls -lah)",
"Bash(py --version)",
"Bash(py -3 --version)",
"Bash(py scripts/import_cmangos_travel_nodes.py)",
"Read(//home/dev/azerothcore_installer/_server/azerothcore/modules/mod-playerbots/data/sql/playerbots/updates/**)",
"Bash(py scripts/fixup_id_collision.py)",
"Bash(py scripts/insert_ignore.py)",
"Bash(grep -nA 6 \"passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Include/DetourNavMeshQuery.h)",
"Bash(grep -nA 5 \"dtQueryFilter::passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp)",
"Bash(grep -nB1 -A2 \"modAlmostUnwalkableTriangles\" C:/Users/Admin/git/main/azerothcore-wotlk/src/tools/mmaps_generator/MapBuilder.cpp)",
"Bash(xargs grep -l \"MoveFarTo\\\\|ResolveMovePath\\\\|DispatchMovement\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.h)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push)",
"Bash(wait)",
"Bash(git -C \"c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots\" show 3710c35a)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" show 9cccc5d26)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\\\\\"\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\\\\\"\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD --stat)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" reset --hard cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push --force-with-lease)",
"Bash(git cherry-pick *)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status -s)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff --stat)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp src/server/game/Movement/MovementGenerators/PathGenerator.h)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Cap Player path filter at 50° + water bias under MOD_PLAYERBOTS\")",
"Bash(grep -n \"CollectIncludeDirectories\\\\|sourcePath\\\\|GLOB.*\\\\\\\\.cpp\\\\\\\\|target_include\" C:/Users/Admin/git/main/azerothcore-wotlk/modules/CMakeLists.txt)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"fix\\(Core/Movement\\): Gate playerbot path filter on WorldSession::IsBot\\(\\)\")",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" fetch origin fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline HEAD..origin/fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" rebase origin/fix/mmaps-config-overrides-and-aliases)",
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline -3)",
"Bash(grep -v \"//\")",
"Bash(grep -v \"^//\")"
],
"additionalDirectories": [
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\tools\\mmaps_generator",
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\server\\game\\Movement\\MovementGenerators"
]
}
}

View File

@ -48,6 +48,16 @@ Then build the server following the platform-specific instructions in our **[Ins
> **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. > **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.
### Required server configuration
In `worldserver.conf` (AzerothCore core config), set:
```ini
PreloadAllNonInstancedMapGrids = 1
```
This is required for `mod-playerbots`.
### Detailed Guides ### Detailed Guides
| Guide | Description | | Guide | Description |

View File

@ -22,7 +22,6 @@
# THRESHOLDS # THRESHOLDS
# QUESTS # QUESTS
# COMBAT # COMBAT
# GREATER BUFFS STRATEGIES
# CHEATS # CHEATS
# SPELLS # SPELLS
# FLIGHTPATH # FLIGHTPATH
@ -343,10 +342,6 @@ AiPlayerbot.MaxWaitForMove = 5000
# 2 - MoveSplinePath disabled everywhere # 2 - MoveSplinePath disabled everywhere
AiPlayerbot.DisableMoveSplinePath = 0 AiPlayerbot.DisableMoveSplinePath = 0
# Max search time for movement (higher for better movement on slopes)
# Default: 3
AiPlayerbot.MaxMovementSearchTime = 3
# Action expiration time # Action expiration time
AiPlayerbot.ExpireActionTime = 5000 AiPlayerbot.ExpireActionTime = 5000
@ -478,6 +473,23 @@ AiPlayerbot.AutoSaveMana = 1
# Default: 60 (60%) # Default: 60 (60%)
AiPlayerbot.SaveManaThreshold = 60 AiPlayerbot.SaveManaThreshold = 60
# Enable Paladin bots to use greater blessings, with the blessing used being based on the
# number of Paladins in the raid/group and the spec of the recipient. Priorities for each
# spec are hardcoded in GreaterBlessingActions.h.
# 0 = disabled
# 1 = enabled in raid groups only
# 2 = enabled in all groups
# Default: 1 (raid only)
AiPlayerbot.AutoGreaterBlessings = 1
# Enable bots to use group reagent buffs: Gift of the Wild, Arcane Brilliance,
# Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection.
# 0 = disabled
# 1 = enabled in raid groups only
# 2 = enabled in all groups
# Default: 2 (all groups)
AiPlayerbot.AutoPartyBuffs = 2
# Bots can flee from enemies # Bots can flee from enemies
AiPlayerbot.FleeingEnabled = 1 AiPlayerbot.FleeingEnabled = 1
@ -486,24 +498,6 @@ AiPlayerbot.FleeingEnabled = 1
# #
#################################################################################################### ####################################################################################################
####################################################################################################
# GREATER BUFFS STRATEGIES
#
#
# Min group size to use Greater buffs (Paladin, Mage, Druid)
# Default: 3
AiPlayerbot.MinBotsForGreaterBuff = 3
# Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff
# Default: 30
AiPlayerbot.RPWarningCooldown = 30
#
#
#
####################################################################################################
#################################################################################################### ####################################################################################################
# CHEATS # CHEATS
# #
@ -1065,6 +1059,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.EnableNewRpgStrategy = 1 AiPlayerbot.EnableNewRpgStrategy = 1
# Use pre-computed travel node paths for long-distance movement (>300 yards).
# When enabled, bots use the travel node graph (A*, flight paths, transports)
# instead of repeated mmap hops. Experimental.
# Default: 0 (disabled)
AiPlayerbot.EnableTravelNodes = 0
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise. # Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
# Sum of weights need not be 100. Set to 0 to disable the status. # Sum of weights need not be 100. Set to 0 to disable the status.
# #

View File

@ -0,0 +1,632 @@
-- Imported from cmangos-playerbots ai_playerbot_travelnode (wotlk)
-- 626 new nodes (cmangos has them, we didn't)
-- Matched on (name, mapId) + closest position; remaining are unique to cmangos.
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
(3781, ' portal', 0, 3476.3600, -4493.3600, 137.4900, 1),
(3782, ' portal', 530, 6107.2600, -6990.6400, 133.3170, 1),
(3783, ' spirithealer', 609, 1886.7800, -5784.5900, 102.8610, 1),
(3784, ' spirithealer', 609, 2116.1900, -5286.9400, 81.2151, 1),
(3785, ' spirithealer', 609, 2364.4200, -5771.3200, 151.3670, 1),
(3786, 'Absalan the Pious', 623, 36.0312, 40.4600, 25.0322, 1),
(3787, 'Acherus: The Ebon Hold flightMaster', 0, 2348.6300, -5669.2900, 382.3240, 1),
(3788, 'Acherus: The Ebon Hold spirithealer', 0, 2356.6500, -5663.0600, 382.2570, 1),
(3789, 'Alliance Log Ride 01 Begin', 571, 4274.5300, -3055.5500, 319.4630, 1),
(3790, 'Alliance PVP Barracks', 449, -9.1189, -4.2670, 5.5710, 1),
(3791, 'Alterac Mountains The Headland', 0, -80.9055, -331.6660, 136.4710, 1),
(3792, 'Alterac Mountains', 0, 498.3190, -1076.6400, 195.8960, 1),
(3793, 'Arathi Highlands The Sanctum', 0, -1532.4300, -1882.5600, 69.5533, 1),
(3794, 'Arathi Highlands The Tower of Arathor', 0, -1774.4800, -1518.3900, 75.2667, 1),
(3795, 'Archmage Pentarus', 603, -718.4560, -57.1132, 429.9240, 1),
(3796, 'Area 52 innkeeper', 530, 3062.1500, 3701.8200, 142.5620, 1),
(3797, 'Area52 Transporter', 530, 3092.5800, 3644.7200, 143.1360, 1),
(3798, 'Ashenvale Kargathia Keep', 1, 2437.0600, -3543.6300, 98.3115, 1),
(3799, 'Auberdine innkeeper', 1, 6406.5100, 515.3670, 8.7257, 1),
(3800, 'Auchindoun: Auchenai Crypts', 558, 63.4074, -175.2640, 15.4378, 1),
(3801, 'Auchindoun: Mana-Tombs', 557, -220.0700, -177.6620, -0.9810, 1),
(3802, 'Auchindoun: Sethekk Halls', 556, -102.4290, 177.5860, 0.0932, 1),
(3803, 'Auchindoun: Shadow Labyrinth', 555, -272.2950, -140.0660, 8.1563, 1),
(3804, 'Balzaphon', 329, 3733.2700, -3480.1100, 131.0400, 1),
(3805, 'Blackfathom Deeps', 48, -570.3640, 0.8977, -47.1378, 1),
(3806, 'Blackrock Depths', 230, 870.1800, -239.3270, -71.6776, 1),
(3807, 'Blackrock Spire', 229, -16.0931, -392.4040, 48.5157, 1),
(3808, 'Blades Edge Mountains Circle of Blood', 530, 2879.1300, 5979.4500, 6.2402, 1),
(3809, 'Blades Edge Mountains Vimgols Circle', 530, 3279.9000, 4640.3600, 216.5280, 1),
(3810, 'Blood elf start', 530, 10349.6000, -6357.2900, 33.4026, 1),
(3811, 'Bloodmyst Isle The Hidden Reef', 530, -1148.6600, -11127.4000, -76.0074, 1),
(3812, 'Borean Tundra Coldarra', 571, 3917.0600, 6817.9200, 150.5070, 1),
(3813, 'Borean Tundra Naxxanar', 571, 3750.9300, 3583.8600, 353.1330, 1),
(3814, 'Burning Steppes Blackrock Mountain', 0, -7996.6300, -1013.4100, 131.8670, 1),
(3815, 'Cannon Charging (Port)', 530, 1920.1300, 5581.9000, 269.7220, 1),
(3816, 'Cannon Prep', 0, -9569.6000, -13.7809, 63.9459, 1),
(3817, 'Cannon Prep', 1, -1327.6600, 85.9815, 130.2070, 1),
(3818, 'Cannon Prep', 530, -1742.2500, 5457.4000, -11.9282, 1),
(3819, 'Chief Thunder-Skins', 230, 847.8390, -181.1150, -49.6707, 1),
(3820, 'Coilfang: Serpentshrine Cavern', 548, 135.1620, -488.7410, 0.8400, 1),
(3821, 'Coilfang: The Slave Pens', 547, -71.6723, -322.2440, -1.5438, 1),
(3822, 'Coilfang: The Steamvault', 545, -91.9829, -255.4900, -12.5306, 1),
(3823, 'Coilfang: The Underbog', 546, 108.0610, -198.7420, 50.1184, 1),
(3824, 'Coilskar Witch', 585, 139.1010, -104.6440, -20.9245, 1),
(3825, 'Crystalsong Forest The Azure Front', 571, 5434.6500, 729.6780, 186.6960, 1),
(3826, 'Crystalsong Forest The Great Tree', 571, 5895.1000, 1022.6900, 185.5390, 1),
(3827, 'Crystalsong Forest The Twilight Rivulet', 571, 5508.9200, 432.9360, 161.7470, 1),
(3828, 'Dalaran Portal to Caverns of Time', 1, -8164.8000, -4768.5000, 34.3000, 1),
(3829, 'Dalaran Portal to Caverns of Time', 571, 5781.4800, 841.2580, 680.3790, 1),
(3830, 'Dalaran Portal to Darnassus', 1, 9656.5400, 2518.2600, 1331.6600, 1),
(3831, 'Dalaran Portal to Darnassus', 571, 5706.1600, 730.1020, 641.7450, 1),
(3832, 'Dalaran Portal to Exodar', 571, 5699.5800, 735.4690, 641.7690, 1),
(3833, 'Dalaran Portal to Ironforge', 0, -4613.7100, -915.2870, 501.0620, 1),
(3834, 'Dalaran Portal to Ironforge', 571, 5712.6800, 724.8450, 641.7360, 1),
(3835, 'Dalaran Portal to Orgrimmar', 1, 1470.0200, -4222.5900, 59.2213, 1),
(3836, 'Dalaran Portal to Orgrimmar', 571, 5925.8500, 593.2500, 640.5630, 1),
(3837, 'Dalaran Portal to Shattrath', 530, -1824.3200, 5417.2300, -12.4277, 1),
(3838, 'Dalaran Portal to Shattrath', 530, -1902.4900, 5442.8600, -12.4280, 1),
(3839, 'Dalaran Portal to Shattrath', 571, 5697.4900, 744.9120, 641.8190, 1),
(3840, 'Dalaran Portal to Shattrath', 571, 5941.6600, 584.8870, 640.5740, 1),
(3841, 'Dalaran Portal to Silvermoon', 530, 9998.4600, -7106.5500, 47.7054, 1),
(3842, 'Dalaran Portal to Silvermoon', 571, 5946.9800, 568.4790, 640.5730, 1),
(3843, 'Dalaran Portal to Stormwind', 571, 5719.1900, 719.6810, 641.7280, 1),
(3844, 'Dalaran Portal to Thunder Bluff', 1, -967.3750, 284.8200, 110.7730, 1),
(3845, 'Dalaran Portal to Thunder Bluff', 571, 5945.8100, 577.3570, 640.5740, 1),
(3846, 'Dalaran Portal to Undercity', 571, 5934.6600, 590.6880, 640.5750, 1),
(3847, 'Dalaran Violet Citadel Balcony', 571, 5865.0800, 840.2100, 846.3330, 1),
(3848, 'Darnassus The Temple Gardens', 1, 9814.7800, 2574.2100, 1314.4700, 1),
(3849, 'Darnassus Warriors Terrace', 1, 9976.2100, 2262.3700, 1334.5000, 1),
(3850, 'Darnassus innkeeper', 1, 9951.8800, 2282.8200, 1345.1600, 1),
(3851, 'Deadwind Pass Groshgok Compound', 0, -11121.6000, -2416.2600, 108.6530, 1),
(3852, 'Deadwind Pass Karazhan', 0, -11123.3000, -2006.7700, 47.2725, 1),
(3853, 'Deadwind Pass', 0, -10377.2000, -1785.4200, 94.3243, 1),
(3854, 'Deeprun Tram', 369, 13.6699, 80.6694, -4.2973, 1),
(3855, 'Desolace Bolgans Hole', 1, -2375.9500, 2469.7600, 74.4626, 1),
(3856, 'Desolace The Veiled Sea', 1, -1869.3900, 3404.3400, -50.7694, 1),
(3857, 'Dire Maul', 429, 135.5790, 192.0480, -3.3910, 1),
(3858, 'Doodad_CF_elevatorPlatform01entry', 548, 50.0000, -0.0071, -70.9017, 1),
(3859, 'Doodad_CF_elevatorPlatform_small01entry', 548, -59.1350, -98.7966, -51.7018, 1),
(3860, 'Doodad_FactoryElevator01entry', 554, 0.5438, -1.3935, -1.7012, 1),
(3861, 'Doodad_HF_Elevator_Gate01entry', 571, 254.1330, -5892.0000, 258.4950, 1),
(3862, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 167.2690, 1),
(3863, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 173.3200, 1),
(3864, 'Doodad_HF_Elevator_Gate02entry', 571, 275.2000, -5918.1300, 166.4950, 1),
(3865, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 291.7530, 1),
(3866, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 286.0490, 1),
(3867, 'Doodad_HF_Elevator_Gate03entry', 571, 133.3330, -5756.2700, 285.5940, 1),
(3868, 'Doodad_HF_Elevator_Lift01entry', 571, 259.9700, -5893.8300, 255.8280, 1),
(3869, 'Doodad_HF_Elevator_Lift01entry', 571, 261.1220, -5910.0100, 77.0138, 1),
(3870, 'Doodad_HF_Elevator_Lift01entry', 571, 261.9400, -5918.9400, 164.0950, 1),
(3871, 'Doodad_HF_Elevator_Lift_01entry', 571, 158.9330, -5760.0000, 38.3942, 1),
(3872, 'Doodad_ID_elevator01entry', 571, 3324.2300, -5134.7400, 300.5890, 1),
(3873, 'Doodad_ID_elevator02entry', 571, 3302.5400, -5103.7300, 300.5890, 1),
(3874, 'Doodad_ID_elevator03entry', 571, 3286.7600, -5135.8800, 300.5890, 1),
(3875, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 336.1010, 1),
(3876, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 330.6150, 1),
(3877, 'Doodad_LogRun_PumpElevator05entry', 571, 4264.2300, -3276.9500, 329.3720, 1),
(3878, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3556.2500, 6928.7200, 251.3120, 1),
(3879, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3979.2300, 7272.5600, 256.0160, 1),
(3880, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 4110.2800, 6755.6900, 249.1790, 1),
(3881, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1213.8600, 1339.0600, 248.2530, 1),
(3882, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1343.4300, 825.8630, 242.8180, 1),
(3883, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 789.8890, 992.0980, 251.2600, 1),
(3884, 'Doodad_Vrykul_Gondola01entry', 571, 698.9930, -3824.2500, 269.5360, 1),
(3885, 'Doodad_Vrykul_Gondola01entryentry', 571, 700.2670, -3823.5000, 269.3790, 1),
(3886, 'Doodad_Vrykul_Gondola_01entry', 575, -557.0330, 1543.4600, -288.9820, 1),
(3887, 'Doodad_icecrown_elevator02entry', 631, 4366.6900, 2365.6200, 358.4790, 1),
(3888, 'Doodad_mushroombase_elevator01entry', 530, 285.6000, 5927.2000, 26.6102, 1),
(3889, 'Doodad_org_arena_axe_pillar01entry', 618, 768.0000, -298.6670, 28.4867, 1),
(3890, 'Doodad_org_arena_lightning_pillar01entry', 618, 768.0000, -277.3330, 28.4867, 1),
(3891, 'Draenei start', 530, -3961.6400, -13931.2000, 100.6150, 1),
(3892, 'Dragonblight Frostmourne Cavern', 571, 4736.3900, -558.6370, 166.0830, 1),
(3893, 'Dragonblight Scarlet Tower', 571, 4692.0500, -356.1290, 178.7370, 1),
(3894, 'Dragonblight The Pit of Narjun', 571, 3740.0300, 2125.2500, 43.0709, 1),
(3895, 'Dragonblight Wintergarde Crypt', 571, 3584.5200, -781.0240, 155.6860, 1),
(3896, 'Dragonblight Wintergarde Mausoleum', 571, 3650.8400, -1123.3200, 89.2307, 1),
(3897, 'Drakuramas Teleport 02', 571, 6175.5900, -2000.6700, 241.7690, 1),
(3898, 'Dun Morogh Chill Breeze Valley', 0, -5550.9700, -76.1191, 426.9310, 1),
(3899, 'Dun Morogh Thunderbrew Distillery', 0, -5595.6700, -513.5950, 409.4150, 1),
(3900, 'Durotar Burning Blade Coven', 1, -88.3030, -4285.0100, 62.0652, 1),
(3901, 'Durotar Dustwind Cave', 1, 877.9880, -4745.6200, 30.4963, 1),
(3902, 'Durotar Razor Hill Barracks', 1, 319.0970, -4812.8800, 10.6054, 1),
(3903, 'Durotar The Den', 1, -588.7030, -4144.9400, 41.1033, 1),
(3904, 'Duskwood Forlorn Rowe', 0, -10320.8000, 368.2760, 60.5037, 1),
(3905, 'Duskwood Rolands Doom', 0, -11106.5000, -1160.6700, 42.2494, 1),
(3906, 'Dustwallow Marsh Emberstrifes Den', 1, -5103.3600, -3949.5300, 41.4934, 1),
(3907, 'Dustwallow Marsh Foothold Citadel', 1, -3721.7400, -4538.6000, 25.9170, 1),
(3908, 'Dustwallow Marsh North Point Tower', 1, -2885.3400, -3419.3200, 47.2420, 1),
(3909, 'Dustwallow Marsh The Great Sea', 1, -4026.5700, -4975.8200, 8.2153, 1),
(3910, 'Dustwallow to Stormwind Teleport', 0, -9008.7900, 851.3200, 105.8900, 1),
(3911, 'Eastern Plaguelands MazraAlor', 0, 3439.2500, -4980.9600, 195.8110, 1),
(3912, 'Eastern Plaguelands Terrorweb Tunnel', 0, 2903.1100, -2614.6700, 89.8071, 1),
(3913, 'Eastern Plaguelands The Noxious Glade', 0, 2714.6600, -5421.4700, 161.4070, 1),
(3914, 'Elevatorentry', 0, -5065.6200, 437.5100, 424.1080, 1),
(3915, 'Elevatorentry', 1, 2261.3300, -5565.5600, 34.2689, 1),
(3916, 'Elevatorentry', 530, 1914.5100, 5514.0200, 280.6880, 1),
(3917, 'Elevatorentry', 571, 2872.9500, 6234.9700, 104.9120, 1),
(3918, 'Elevatorentry', 571, 2894.6900, 6244.8900, 209.1970, 1),
(3919, 'Elevatorentry', 571, 4185.2900, 5283.6200, 39.6833, 1),
(3920, 'Elwynn Forest Thunder Falls', 0, -9291.1000, 677.6830, 131.7780, 1),
(3921, 'Entangle', 531, -8003.0000, 1222.9000, -82.1000, 1),
(3922, 'Entangle', 531, -8022.3000, 1149.0000, -89.1000, 1),
(3923, 'Entangle', 531, -8043.6000, 1254.1000, -84.3000, 1),
(3924, 'Eredar Soul-Eater', 552, 285.5190, 146.1550, 22.3118, 1),
(3925, 'Escape Voltarus', 571, 5875.4300, -1981.3700, 234.6710, 1),
(3926, 'Escape to the Isle of QuelDanas', 530, 12887.6000, -6869.2100, 10.1141, 1),
(3927, 'Escape to the Isle of QuelDanas', 585, 148.4010, 203.4430, -11.9579, 1),
(3928, 'Evergrove innkeeper', 530, 3022.9000, 5435.5900, 146.7010, 1),
(3929, 'Everlook Transporter', 1, 6755.2200, -4658.0400, 724.7950, 1),
(3930, 'Everlook innkeeper', 1, 6695.1500, -4673.0400, 721.6500, 1),
(3931, 'Eversong Woods Commons Hall', 530, 9822.0000, -6694.3700, 2.5945, 1),
(3932, 'Eversong Woods Falthrien Academy', 530, 10158.0000, -6026.5000, 63.7448, 1),
(3933, 'Eversong Woods Huntress of the Sun', 530, 9699.4300, -6701.3600, -0.2076, 1),
(3934, 'Exit Portal', 571, 3877.1400, 6980.8300, 152.0400, 1),
(3935, 'Feathermoon Stronghold innkeeper', 1, -4381.5900, 3289.4500, 13.6266, 1),
(3936, 'Felwood Bloodvenom River', 1, 5288.7400, -544.2570, 328.6870, 1),
(3937, 'Felwood Irontree Cavern', 1, 6353.3200, -1696.9200, 440.0420, 1),
(3938, 'Felwood Shrine of the Deceiver', 1, 4779.2600, -572.0400, 275.8440, 1),
(3939, 'Gadgetzan Transporter', 1, -7109.1200, -3825.2100, 10.1529, 1),
(3940, 'Gadgetzan innkeeper', 1, -7158.9600, -3841.6100, 8.8481, 1),
(3941, 'Gallows End Tavern innkeeper', 0, 2269.5100, 244.9440, 34.3402, 1),
(3942, 'Gates of AhnQiraj', 1, -8233.1200, 1535.7500, -0.3744, 1),
(3943, 'Ghostlands Deatholme', 530, 6465.7700, -6433.6600, 50.4155, 1),
(3944, 'Ghostlands Thalassian Pass', 530, 6554.0200, -6811.2000, 110.6000, 1),
(3945, 'Ghostly Baker', 532, -11057.7000, -1919.8300, 77.3515, 1),
(3946, 'Gnomeregan', 90, -617.4620, 370.6960, -247.1880, 1),
(3947, 'Gravity Lapse - Center Teleport', 585, 148.5000, 181.0000, -16.7000, 1),
(3948, 'Greater Shadowbat', 532, -10935.0000, -1999.5000, 49.4748, 1),
(3949, 'Grizzly Hills - Quest - Arugal Teleport Back', 571, 3841.4000, -3426.6500, 293.1040, 1),
(3950, 'Grizzly Hills Boulder Hills', 571, 5082.6400, -4724.1900, 287.5000, 1),
(3951, 'Grizzly Hills Duskhowl Den', 571, 3992.0100, -4523.4300, 195.6070, 1),
(3952, 'Grizzly Hills Ursocs Den', 571, 4692.7100, -3863.7500, 327.3770, 1),
(3953, 'Grizzly Hills Voldrune Dwelling', 571, 3005.7300, -2610.3100, 98.5530, 1),
(3954, 'Gruuls Lair', 565, 107.8710, 282.5120, 1.9718, 1),
(3955, 'Gunship Portal', 628, 747.0000, -1075.0000, 135.0000, 1),
(3956, 'Gunship Portal', 641, 16.4763, 0.0184, 20.4162, 1),
(3957, 'Gunship Portal', 642, 12.3199, 0.0963, 34.6508, 1),
(3958, 'Heart of the Deconstructor', 603, 886.2750, -12.0545, 409.6020, 1),
(3959, 'Hellfire Citadel: The Blood Furnace', 542, 331.0690, 28.0790, 9.7047, 1),
(3960, 'Hellfire Citadel: The Shattered Halls', 540, 203.6690, 157.2020, -42.3511, 1),
(3961, 'Hellfire Peninsula The Path of Anguish', 530, -525.2730, 2066.0700, 94.5205, 1),
(3962, 'Hex Lord Malacrass', 568, 117.3630, 923.5690, 33.9726, 1),
(3963, 'Hillsbrad Foothills Durnholde Keep', 0, -489.8100, -1480.9400, 88.1965, 1),
(3964, 'HiveZara Swarmer Teleport', 509, -9757.8700, 1416.7100, 76.7664, 1),
(3965, 'HiveZara Swarmer Teleport', 509, -9778.9100, 1419.9800, 61.0743, 1),
(3966, 'HiveZara Swarmer Teleport', 509, -9805.9500, 1422.8500, 77.5852, 1),
(3967, 'HiveZara Swarmer Teleport', 509, -9827.5800, 1506.2800, 82.3052, 1),
(3968, 'HiveZara Swarmer Teleport', 509, -9829.4200, 1456.3700, 90.7015, 1),
(3969, 'Horde Log Ride 01 Begin', 571, 4313.3700, -2958.1700, 318.4630, 1),
(3970, 'Howling Fjord New Agamand Inn', 571, 456.4610, -4519.2400, 244.9780, 1),
(3971, 'Howling Fjord Utgarde Keep', 571, 855.8150, -4831.8500, -115.7170, 1),
(3972, 'Howling Fjord Westguard Keep', 571, 1385.1700, -3244.6400, 162.8370, 1),
(3973, 'Howling Fjord Wildervar Mine', 571, 2604.4700, -5019.1400, 293.8260, 1),
(3974, 'Icecrown Jotunheim', 571, 7220.9200, 4113.1400, 630.8240, 1),
(3975, 'Icecrown Kulgalar Keep', 571, 6831.2300, 3817.0800, 621.1530, 1),
(3976, 'Icecrown Nazanak: The Forgotten Depths', 571, 5853.5100, 1807.1700, -344.9310, 1),
(3977, 'Icecrown The Fleshwerks', 571, 6610.5800, 3359.6700, 660.4740, 1),
(3978, 'Indisposed II', 571, 3454.1100, -2802.3700, 202.1400, 1),
(3979, 'Ironforge The Forlorn Cavern', 0, -4636.2200, -1110.0000, 501.4110, 1),
(3980, 'Ironforge flightMaster', 0, -4821.1300, -1152.4000, 502.2950, 1),
(3981, 'Ironforge innkeeper', 0, -4840.6700, -857.0940, 501.9970, 1),
(3982, 'Ironforge innkeeper', 0, -4908.1700, -970.9860, 505.2260, 1),
(3983, 'Ironforge', 0, -4832.2700, -1069.6400, 502.2680, 1),
(3984, 'Kalecgos', 585, 197.8630, -272.7440, -8.6516, 1),
(3985, 'Lady Faltheress', 129, 2583.1800, 695.8610, 56.8033, 1),
(3986, 'Lady Liadrin', 580, 1720.0200, 643.2330, 28.1335, 1),
(3987, 'Loch Modan Stoutlager Inn', 0, -5391.7300, -2974.8800, 325.9470, 1),
(3988, 'Lord Blackwood', 289, 200.2010, 150.8390, 109.8790, 1),
(3989, 'MaiKyl', 230, 842.7150, -181.5710, -49.6700, 1),
(3990, 'Majordomo Teleport', 409, 848.9330, -812.8750, -229.6010, 1),
(3991, 'Maraudon Portal Effect', 349, 386.2700, 33.4144, -130.9340, 1),
(3992, 'Maraudon', 349, 614.7090, -206.5360, -64.2963, 1),
(3993, 'Mesa Elevator', 209, 2600.8100, 1228.7200, -40.2788, 1),
(3994, 'Mesa Elevator', 209, 2600.8100, 1228.7200, 89.0211, 1),
(3995, 'Mesa Elevator', 209, 2617.5100, 1243.9200, -40.5284, 1),
(3996, 'Mesa Elevator', 209, 2617.5100, 1243.9200, 89.0253, 1),
(3997, 'Mesa Elevatorentry', 1, -1030.1400, -32.0337, 68.2329, 1),
(3998, 'Mesa Elevatorentry', 1, -1032.6600, -26.3241, 141.1470, 1),
(3999, 'Mesa Elevatorentry', 1, -1035.1900, -45.0725, 68.2586, 1),
(4000, 'Mesa Elevatorentry', 1, -1042.0500, -47.7792, 141.1470, 1),
(4001, 'Mesa Elevatorentry', 1, -1284.7700, 185.1110, 130.3230, 1),
(4002, 'Mesa Elevatorentry', 1, -1290.7100, 188.5430, 67.4105, 1),
(4003, 'Mesa Elevatorentry', 1, -1304.5600, 186.1990, 67.5106, 1),
(4004, 'Mesa Elevatorentry', 1, -1308.1800, 180.4040, 130.2800, 1),
(4005, 'Mesa Elevatorentry', 1, -4660.6000, -1828.2700, 85.6823, 1),
(4006, 'Mesa Elevatorentry', 1, -4666.2300, -1851.2600, 85.6813, 1),
(4007, 'Mesa Elevatorentry', 1, -4666.4800, -1832.0900, -45.3171, 1),
(4008, 'Mesa Elevatorentry', 1, -4670.1200, -1845.7300, -45.1181, 1),
(4009, 'Mesa Elevatorentry', 1, -5385.2000, -2485.3300, 89.4297, 1),
(4010, 'Mesa Elevatorentry', 1, -5385.2700, -2492.0100, -41.7147, 1),
(4011, 'Mesa Elevatorentry', 1, -5395.6800, -2501.7500, -41.5869, 1),
(4012, 'Mesa Elevatorentry', 1, -5402.5700, -2501.2400, 89.4297, 1),
(4013, 'Mesa Elevatorentry', 209, 2597.4300, 1232.1000, 89.4297, 1),
(4014, 'Mesa Elevatorentry', 209, 2604.3200, 1231.5800, -41.5870, 1),
(4015, 'Mesa Elevatorentry', 209, 2614.7300, 1241.3200, -41.7145, 1),
(4016, 'Mesa Elevatorentry', 209, 2614.8000, 1248.0000, 89.4297, 1),
(4017, 'Mesa Elevatorentry', 47, 1729.8800, 1354.2700, -45.1181, 1),
(4018, 'Mesa Elevatorentry', 47, 1733.5200, 1367.9100, -45.3171, 1),
(4019, 'Mesa Elevatorentry', 47, 1733.7700, 1348.7400, 85.6813, 1),
(4020, 'Mesa Elevatorentry', 47, 1739.4000, 1371.7300, 85.6823, 1),
(4021, 'Mole Machine Port to Grim Guzzler', 230, 901.0680, -143.9390, -49.7550, 1),
(4022, 'Molten Core', 409, 889.0990, -822.9660, -227.2440, 1),
(4023, 'Move Bind Sight', 230, 824.8090, -176.1660, -49.7551, 1),
(4024, 'Mudsprocket innkeeper', 1, -4629.4300, -3176.1400, 41.2339, 1),
(4025, 'Murta Grimgut', 209, 1891.0700, 1294.7800, 48.2347, 1),
(4026, 'Nagrand Abandoned Armory', 530, -2052.0900, 7432.7600, -24.9648, 1),
(4027, 'Naxxanar portal', 571, 3692.4300, 3577.9400, 473.3200, 1),
(4028, 'Naxxanar portal', 571, 3734.1400, 3571.1200, 341.6620, 1),
(4029, 'Naxxanar portal', 571, 3738.6400, 3567.6700, 294.5220, 1),
(4030, 'Naxxanar portal', 571, 3739.5000, 3567.0000, 337.5640, 1),
(4031, 'Naxxanar portal', 571, 3739.6600, 3567.2900, 286.7850, 1),
(4032, 'Naxxanar portal', 571, 3787.5700, 3558.1100, 469.3220, 1),
(4033, 'Naxxanar portal', 571, 3801.5000, 3586.0500, 49.5700, 1),
(4034, 'Naxxramas Teleport - Sapphiron Exit', 533, 3005.7300, -3414.7600, 297.0260, 1),
(4035, 'Netherstorm Manaforge Ara', 530, 3965.9500, 3911.9700, 178.4460, 1),
(4036, 'Netherstorm The Violet Tower', 530, 2243.2600, 2243.1400, 101.5590, 1),
(4037, 'Nexus Portal', 578, 1045.5700, 1104.2400, 361.0700, 1),
(4038, 'Nexus Portal', 578, 982.1730, 1055.4500, 359.9670, 1),
(4039, 'Nijels Point innkeeper', 1, 255.6150, 1253.7600, 192.2240, 1),
(4040, 'No Mans Land -> Out of Hyjal', 1, 5010.1700, -4554.9400, 852.1460, 1),
(4041, 'Onyxias Lair', 249, -90.7180, -106.2340, -38.1972, 1),
(4042, 'Orb of the Nexus', 578, 1048.2700, 991.3100, 361.0700, 1),
(4043, 'Orgrims Hammer', 622, 23.4324, 0.0201, 33.5795, 1),
(4044, 'Orgrims Hammerdock', 668, 5223.4700, 1537.8700, 645.6480, 1),
(4045, 'Orgrimmar flightMaster', 1, 1676.2500, -4313.4500, 61.9445, 1),
(4046, 'Out of Body Experience', 0, -465.6990, 1495.9800, 17.3595, 1),
(4047, 'Port to Mazthoril', 1, 5904.2000, -4045.9000, 596.4300, 1),
(4048, 'Portal Effect: Acherus', 609, 2400.0300, -5635.0000, 377.0170, 1),
(4049, 'Portal of Immolthar', 429, -37.5643, 813.4330, -7.4382, 1),
(4050, 'Portal to Blasted Lands', 0, -11708.4000, -3168.0000, -5.0700, 1),
(4051, 'Portal to Blasted Lands', 0, -4606.4400, -928.9970, 501.0700, 1),
(4052, 'Portal to Blasted Lands', 0, -9007.5800, 871.8700, 129.6920, 1),
(4053, 'Portal to Blasted Lands', 0, 1768.7700, 55.3698, -46.3194, 1),
(4054, 'Portal to Blasted Lands', 1, -944.0640, 274.8590, 111.7100, 1),
(4055, 'Portal to Blasted Lands', 1, 1472.0900, -4214.6300, 59.2208, 1),
(4056, 'Portal to Blasted Lands', 1, 9661.8300, 2509.6000, 1331.6300, 1),
(4057, 'Portal to Blasted Lands', 530, -4039.0700, -11555.1000, -138.3370, 1),
(4058, 'Portal to Blasted Lands', 530, 9984.0300, -7108.4300, 47.7049, 1),
(4059, 'Portal to Dalaran', 571, 5807.7500, 588.3470, 661.5050, 1),
(4060, 'Portal to Dalaran', 631, -72.7031, 2282.4500, 32.8673, 1),
(4061, 'Portal to Dalaran', 712, 4.3953, 13.6833, 20.8039, 1),
(4062, 'Portal to Dalaran', 713, 22.1770, 22.9527, 35.6576, 1),
(4063, 'Portal to Orgrimmar', 1, 1321.8100, -4383.1900, 26.2300, 1),
(4064, 'Portal to Orgrimmar', 1, 1921.3700, -4149.2400, 40.4075, 1),
(4065, 'Portal to Stormwind', 0, -8446.8300, 339.9310, 121.3290, 1),
(4066, 'Portal to Stormwind', 0, -9143.5800, 375.1030, 90.6907, 1),
(4067, 'Portal to The Purple Parlor', 571, 5822.3500, 837.0760, 680.6570, 1),
(4068, 'Portal to The Purple Parlor', 571, 5848.4800, 853.7060, 843.1820, 1),
(4069, 'Portal to The Violet Citadel', 571, 5413.0100, 2868.1800, 418.6750, 1),
(4070, 'Portal to The Violet Citadel', 571, 5819.2600, 829.7740, 680.2200, 1),
(4071, 'Portal to Undercity', 0, 1774.8000, 761.1270, 55.0477, 1),
(4072, 'Portal to Undercity', 0, 1962.6900, 235.9200, 39.7700, 1),
(4073, 'Portal: Moonglade Effect', 1, 7828.8400, -2245.6500, 463.7070, 1),
(4074, 'Portal: Return from Moonglade Effect', 571, 6215.5400, -8.4001, 410.1650, 1),
(4075, 'Portal: Valley of Echoes', 571, 6390.5300, 237.0120, 395.8130, 1),
(4076, 'Quest - Port Skettis Prisoner - Location 01', 530, -4106.6400, 3029.7600, 344.8770, 1),
(4077, 'Quest - Port Skettis Prisoner - Location 02', 530, -3720.3500, 3789.9100, 302.8880, 1),
(4078, 'Quest - Port Skettis Prisoner - Location 03', 530, -3669.5700, 3386.7400, 312.9550, 1),
(4079, 'Quest - Teleport: Caverns of Time', 1, -8380.5000, -4262.5600, -207.5340, 1),
(4080, 'Quetzlun', 571, 5716.2600, -4369.3400, 385.8850, 1),
(4081, 'Ragefire Chasm', 389, -223.4540, 87.2837, -24.9351, 1),
(4082, 'Raven', 209, 1886.6400, 1299.1100, 48.3146, 1),
(4083, 'Recall', 30, -1347.0000, -292.0000, 91.0000, 1),
(4084, 'Recall', 30, 648.0000, -34.0000, 47.0000, 1),
(4085, 'Redridge Mountains Renders Rock', 0, -8737.4300, -2213.5000, 149.9600, 1),
(4086, 'Redridge Mountains Stonewatch', 0, -9392.0000, -3026.6300, 136.7700, 1),
(4087, 'Return to Temper', 530, -3286.6300, -12908.6000, 11.7562, 1),
(4088, 'Revanchion', 429, -106.3030, 551.2760, -4.3970, 1),
(4089, 'Ritual Preparation', 575, 296.6890, -346.5040, 90.5482, 1),
(4090, 'Ritual of the Sword', 575, 296.6890, -346.5040, 108.5480, 1),
(4091, 'Rizzles Escape', 1, 3697.2000, -3967.1300, 28.3115, 1),
(4092, 'Sacrifice', 429, -25.9536, -448.2270, -36.0912, 1),
(4093, 'Sacrifice', 532, -11234.2000, -1698.4600, 179.2400, 1),
(4094, 'Sacrifice', 553, 4.0000, 608.0000, -13.8165, 1),
(4095, 'Samuro', 230, 847.6740, -175.8690, -49.6706, 1),
(4096, 'Scaffold Carsentry', 0, -6889.3100, -1211.7600, 240.4440, 1),
(4097, 'Scaffold Carsentry', 0, -6895.5000, -1338.1500, 240.2840, 1),
(4098, 'Scaffold Carsentry', 0, -6899.7000, -1207.6000, 240.4100, 1),
(4099, 'Scaffold Carsentry', 0, -6903.5800, -1201.6700, 193.8930, 1),
(4100, 'Scaffold Carsentry', 0, -6905.7300, -1211.8300, 214.0430, 1),
(4101, 'Scaffold Carsentry', 469, -6889.3100, -1211.7600, 240.4440, 1),
(4102, 'Scaffold Carsentry', 469, -6895.5000, -1338.1500, 240.2840, 1),
(4103, 'Scaffold Carsentry', 469, -6899.7000, -1207.6000, 240.4100, 1),
(4104, 'Scaffold Carsentry', 469, -6903.5800, -1201.6700, 193.8930, 1),
(4105, 'Scaffold Carsentry', 469, -6905.7300, -1211.8300, 214.0430, 1),
(4106, 'Scaffold Carsentry', 530, -3292.8900, 1901.2800, 142.1590, 1),
(4107, 'Scaffold Carsentry', 530, -3297.0700, 1898.6700, 101.3760, 1),
(4108, 'Scaffold Carsentry', 530, -3300.1400, 1902.2300, 168.4120, 1),
(4109, 'Scaffold Carsentry', 530, -3301.8300, 1895.4800, 122.1590, 1),
(4110, 'Scaffold Carsentry', 530, -3306.6700, 1902.6700, 168.5590, 1),
(4111, 'Scaffold Carsentry', 571, 8115.1400, -245.3940, 900.1890, 1),
(4112, 'Scaffold Carsentry', 571, 8118.4000, -380.0000, 1027.9200, 1),
(4113, 'Scaffold Carsentry', 571, 8118.7700, -386.8160, 981.7890, 1),
(4114, 'Scaffold Carsentry', 571, 8119.4000, -256.0000, 926.3570, 1),
(4115, 'Scaffold Carsentry', 571, 8119.6800, -379.0410, 1028.0500, 1),
(4116, 'Scaffold Carsentry', 571, 8122.6000, -247.5780, 926.3930, 1),
(4117, 'Scaffold Carsentry', 571, 8123.9300, -240.0710, 880.1890, 1),
(4118, 'Scaffold Carsentry', 571, 8127.2900, -380.4440, 1001.7900, 1),
(4119, 'Scroll of Recall', 0, -103.9880, -902.7950, 55.5340, 1),
(4120, 'Scroll of Recall', 0, -10446.9000, -3261.9100, 20.1790, 1),
(4121, 'Scroll of Recall', 0, -5506.3400, -704.3480, 392.6860, 1),
(4122, 'Scroll of Recall', 0, -9470.7600, 3.9090, 49.7940, 1),
(4123, 'Scroll of Recall', 0, 1804.8400, 196.3220, 70.3990, 1),
(4124, 'Scroll of Recall', 0, 286.3140, -2184.0900, 122.6120, 1),
(4125, 'Scroll of Recall', 1, -1060.2700, 23.1370, 141.4550, 1),
(4126, 'Scroll of Recall', 1, -506.2240, -2590.0800, 113.1500, 1),
(4127, 'Scroll of Recall', 1, -7135.7200, -3787.7700, 8.7990, 1),
(4128, 'Scroll of Recall', 1, 6395.7100, 433.2560, 33.2600, 1),
(4129, 'Scryers Tier innkeeper', 530, -2184.0400, 5399.6200, 51.9658, 1),
(4130, 'Searing Gorge Blackchar Cave', 0, -7249.7500, -814.5230, 296.6230, 1),
(4131, 'Searing Gorge Blackrock Mountain', 0, -7243.0600, -1122.9400, 274.6300, 1),
(4132, 'Searing Gorge The Slag Pit', 0, -6601.6300, -1250.7500, 188.0640, 1),
(4133, 'Sewer Teleport 01', 571, 5881.2000, 666.5000, 615.7940, 1),
(4134, 'Sewer Teleport 04', 571, 5815.3000, 714.6000, 619.0980, 1),
(4135, 'Shadow Port', 33, -103.4600, 2122.1000, 155.6550, 1),
(4136, 'Shadow Port', 33, -105.6540, 2154.9800, 156.4300, 1),
(4137, 'Shadow Port', 33, -84.9900, 2151.0100, 155.6200, 1),
(4138, 'Shadowblink', 469, -7469.9300, -1227.9300, 476.7770, 1),
(4139, 'Shadowblink', 469, -7486.3600, -1194.3200, 476.8000, 1),
(4140, 'Shadowblink', 469, -7500.7000, -1249.8900, 476.7980, 1),
(4141, 'Shadowblink', 469, -7506.5800, -1165.2600, 476.7960, 1),
(4142, 'Shadowblink', 469, -7524.3600, -1219.1200, 476.7940, 1),
(4143, 'Shadowblink', 469, -7538.6300, -1273.6400, 476.8000, 1),
(4144, 'Shadowblink', 469, -7542.4700, -1191.9200, 476.3550, 1),
(4145, 'Shadowblink', 469, -7561.5400, -1244.0100, 476.8000, 1),
(4146, 'Shadowblink', 469, -7581.1100, -1216.1900, 476.8000, 1),
(4147, 'Shadowmoon Darkcaster', 540, 72.4708, 184.4520, -13.2380, 1),
(4148, 'Shadowmoon Valley Oronoks Farm', 530, -2802.3200, 1286.4600, 77.3836, 1),
(4149, 'Shattrath Banish Teleport', 530, -1500.0300, 5217.1400, 32.4600, 1),
(4150, 'Shattrath City Aldor Rise', 530, -1751.2400, 5641.2300, 129.0710, 1),
(4151, 'Shattrath City Scryers Tier', 530, -2041.2900, 5561.9700, 54.4371, 1),
(4152, 'Shattrath City innkeeper', 530, -1903.7900, 5765.7000, 131.2960, 1),
(4153, 'Shattrath Portal to Darnassus', 530, -1790.9800, 5413.9800, -12.4282, 1),
(4154, 'Shattrath Portal to Exodar', 530, -1880.2800, 5357.5300, -12.4281, 1),
(4155, 'Shattrath Portal to Exodar', 530, -4031.2400, -11569.6000, -138.2990, 1),
(4156, 'Shattrath Portal to Ironforge', 530, -1795.7900, 5399.6300, -12.4281, 1),
(4157, 'Shattrath Portal to Orgrimmar', 530, -1934.4900, 5453.4800, -12.4279, 1),
(4158, 'Shattrath Portal to Silvermoon', 530, -1894.6900, 5362.3400, -12.4282, 1),
(4159, 'Shattrath Portal to Stormwind', 0, -8998.1400, 861.2540, 29.6206, 1),
(4160, 'Shattrath Portal to Stormwind', 530, -1792.7800, 5406.5400, -12.4279, 1),
(4161, 'Shattrath Portal to Thunder Bluff', 530, -1936.3200, 5445.9500, -12.4282, 1),
(4162, 'Shattrath Portal to Undercity', 0, 1773.4200, 61.7391, -46.3215, 1),
(4163, 'Shattrath Portal to Undercity', 530, -1931.4800, 5460.4900, -12.4281, 1),
(4164, 'Ship (The Bravery)dock', 0, -8644.6400, 1333.7100, 5.7993, 1),
(4165, 'Ship (The Bravery)dock', 1, 6419.1900, 818.6660, 6.1894, 1),
(4166, 'Ship (The Lady Mehley)dock', 0, -3898.2700, -597.7260, 5.5310, 1),
(4167, 'Ship (The Lady Mehley)dock', 1, -4006.9700, -4730.3300, 5.5366, 1),
(4168, 'Ship (The Maidens Fancy)dock', 0, -14279.5000, 571.2000, 6.0777, 1),
(4169, 'Ship (The Maidens Fancy)dock', 1, -997.3520, -3830.2500, 5.5803, 1),
(4170, 'Ship, Icebreaker (Northspear)dock', 571, 584.0140, -5098.6700, -6.4857, 1),
(4171, 'Ship, Night Elf (Elunes Blessing)dock', 1, 6546.9800, 927.7040, 6.1894, 1),
(4172, 'Ship, Night Elf (Elunes Blessing)dock', 530, -4264.1400, -11328.3000, 6.0035, 1),
(4173, 'Ship, Night Elf (Feathermoon Ferry)dock', 1, -4210.9100, 3280.7300, 5.7750, 1),
(4174, 'Ship, Night Elf (Moonspray)dock', 1, 6584.1600, 773.2750, 6.0986, 1),
(4175, 'Ship, Night Elf (Moonspray)dock', 1, 8545.0900, 1016.1700, 6.2912, 1),
(4176, 'Sholazar Basin Nesingwary Base Camp', 571, 5559.1100, 5765.4300, -77.9385, 1),
(4177, 'Sholazar Basin The Seabreach Flow', 571, 5527.8500, 5348.4900, -134.4620, 1),
(4178, 'Silithus Ortells Hideout', 1, -7578.7200, 196.9140, 11.5488, 1),
(4179, 'Silithus Twilights Run', 1, -6265.9300, 41.8866, 9.0625, 1),
(4180, 'Silverpine Forest Fenris Keep', 0, 993.8510, 690.0600, 74.8984, 1),
(4181, 'Sister Mercydock', 571, 254.1330, -3752.8000, 0.2291, 1),
(4182, 'Sister Mercydock', 571, 93.8904, -3681.4100, 0.2103, 1),
(4183, 'Skadi Teleport', 575, 476.7990, -511.1670, 104.7230, 1),
(4184, 'Skarthis the Summoner', 547, -76.9917, -157.0810, -2.1064, 1),
(4185, 'Socrethar Return Portal Effect', 530, 4778.4600, 3455.3600, 104.1300, 1),
(4186, 'Stonetalon Mountains Boulderslide Ravine', 1, -97.6734, 185.5910, 96.6757, 1),
(4187, 'Stonetalon Mountains Cragpool Lake', 1, 1605.9900, 96.7067, 98.5654, 1),
(4188, 'Stonetalon Mountains Windshear Mine', 1, 935.9090, -302.7410, 0.0220, 1),
(4189, 'Storm Peaks', 571, 8974.3700, -1280.5600, 1059.0100, 1),
(4190, 'Stormwind Stockade', 34, 131.1490, 3.3395, -25.5229, 1),
(4191, 'Stormwind to Dustwallow Teleport', 1, -3722.9100, -4413.9600, 26.1300, 1),
(4192, 'Stoutlager Inn innkeeper', 0, -5377.9100, -2973.9100, 323.2520, 1),
(4193, 'Stranglethorn Vale Kurzens Compound', 0, -11571.3000, -644.0530, 31.2554, 1),
(4194, 'Stranglethorn Vale The Vile Reef', 0, -12211.5000, 644.0560, -67.1350, 1),
(4195, 'Subwayentry', 369, -1.0668, -11.3475, -3.9157, 1),
(4196, 'Subwayentry', 369, -1.0668, 2470.3000, -3.9157, 1),
(4197, 'Subwayentry', 369, -39.7335, 9.7886, -3.9157, 1),
(4198, 'Subwayentry', 369, -39.7335, -10.3899, -3.9157, 1),
(4199, 'Subwayentry', 369, -39.7335, 30.3816, -3.9157, 1),
(4200, 'Subwayentry', 369, -50.9334, 2472.9300, -3.9157, 1),
(4201, 'Subwayentry', 369, -50.9334, 2512.1500, -3.9157, 1),
(4202, 'Subwayentry', 369, -51.0097, 2492.2300, -3.9157, 1),
(4203, 'Subwayentry', 369, 10.1333, 2510.4900, -3.9157, 1),
(4204, 'Subwayentry', 369, 10.1333, 2490.7400, -3.9157, 1),
(4205, 'Subwayentry', 369, 10.1333, 8.8000, -3.9157, 1),
(4206, 'Subwayentry', 369, 10.1896, 28.7706, -3.9157, 1),
(4207, 'Summon Menagerie', 578, 1116.1100, 1075.1700, 508.3490, 1),
(4208, 'Summon Menagerie', 578, 1163.7200, 1170.9900, 527.3220, 1),
(4209, 'Summon Menagerie', 578, 968.7080, 1042.4900, 527.3220, 1),
(4210, 'Sunken Temple', 109, -455.3780, 76.5158, -93.3827, 1),
(4211, 'Surface Portal', 571, 5795.8800, 2070.8700, -344.0460, 1),
(4212, 'Surface Portal', 571, 6203.8700, 2262.1700, 497.1970, 1),
(4213, 'Surge Needle Teleporter', 571, 3444.3800, 2361.8700, 38.7409, 1),
(4214, 'Swamp of Sorrows Stagalbog Cave', 0, -10919.3000, -3673.0500, 11.0245, 1),
(4215, 'Swamp of Sorrows Stonard', 0, -10437.8000, -3307.0400, 20.4499, 1),
(4216, 'Teldrassil Banethil Barrow Den', 1, 9794.8400, 1549.1500, 1262.8000, 1),
(4217, 'Teldrassil Fel Rock', 1, 10124.0000, 1115.9100, 1322.8200, 1),
(4218, 'Teldrassil Shadowthread Cave', 1, 10889.4000, 917.5860, 1326.6400, 1),
(4219, 'Teleport - Coldarra, Transitus Shield to Amber Ledge', 571, 3594.1800, 5997.5100, 136.2150, 1),
(4220, 'Teleport - Dalaran to Wintergrasp', 571, 5325.0600, 2843.3600, 409.2850, 1),
(4221, 'Teleport Darkshire', 0, -10566.0000, -1189.0000, 28.0000, 1),
(4222, 'Teleport Defenders', 607, 1226.4300, -71.9616, 70.0842, 1),
(4223, 'Teleport Duskwood', 0, -10368.0000, -422.0000, 64.1214, 1),
(4224, 'Teleport Elwynn', 0, -9104.0000, -70.0000, 83.0000, 1),
(4225, 'Teleport Goldshire', 0, -9464.0000, 62.0000, 56.0000, 1),
(4226, 'Teleport Left', 533, 2692.0000, -3399.2700, 267.6860, 1),
(4227, 'Teleport Moonbrook', 0, -11020.0000, 1436.0000, 43.6892, 1),
(4228, 'Teleport Players on Victory', 631, -548.9830, 2211.2400, 539.2900, 1),
(4229, 'Teleport Return', 533, 2685.0600, -3502.3700, 261.3150, 1),
(4230, 'Teleport Right', 533, 2692.0000, -3321.8600, 267.6860, 1),
(4231, 'Teleport Sanctum Moon - Down', 530, 7513.6300, -6388.9300, 23.8000, 1),
(4232, 'Teleport Sanctum Sun - Down', 530, 7199.4000, -7097.3600, 66.9700, 1),
(4233, 'Teleport To Zone In', 616, 728.0550, 1329.0300, 275.0000, 1),
(4234, 'Teleport Violet Citadel Spire Down', 571, 5790.0000, 734.0000, 640.0000, 1),
(4235, 'Teleport Westfall', 0, -10643.0000, 1052.0000, 33.8437, 1),
(4236, 'Teleport and Transform', 580, 1667.6400, 633.4660, 28.0500, 1),
(4237, 'Teleport back to Main Room', 603, 1970.6100, -25.5988, 324.5500, 1),
(4238, 'Teleport from Azshara Tower', 1, 3641.0000, -4702.0000, 121.0000, 1),
(4239, 'Teleport to Ashtongue NPCs', 564, 702.2200, 200.3000, 125.0100, 1),
(4240, 'Teleport to Center', 568, -34.3160, 1149.6400, 19.1550, 1),
(4241, 'Teleport to Chamber Illusion', 603, 2043.1200, -25.6981, 239.7210, 1),
(4242, 'Teleport to CoT Stratholme Phase 4', 595, 2071.5500, 1287.6800, 141.6870, 1),
(4243, 'Teleport to Council', 564, 603.4200, 305.9820, 271.9000, 1),
(4244, 'Teleport to Final Chamber Effect DND', 531, -8632.8400, 2055.8700, 108.8600, 1),
(4245, 'Teleport to Gnomeregan', 0, -5095.0000, 757.0000, 261.0000, 1),
(4246, 'Teleport to Hall of Command', 0, 2402.6200, -5633.2800, 377.0210, 1),
(4247, 'Teleport to Heart of Acherus', 609, 2419.9100, -5620.4800, 420.6440, 1),
(4248, 'Teleport to Icecrown Illusion', 603, 1949.1300, -80.6744, 239.9900, 1),
(4249, 'Teleport to Inside Violet Hold', 608, 1857.2400, 803.8770, 44.0085, 1),
(4250, 'Teleport to Lake Wintergrasp', 571, 4561.5800, 2835.3300, 389.7900, 1),
(4251, 'Teleport to Lake Wintergrasp', 571, 5025.7100, 3673.4100, 362.6870, 1),
(4252, 'Teleport to Lake Wintergrasp', 571, 5094.6700, 2170.3300, 365.6010, 1),
(4253, 'Teleport to Lake Wintergrasp', 571, 5386.0500, 2840.9700, 418.6750, 1),
(4254, 'Teleport to Molten Core DND', 409, 1080.0000, -483.0000, -108.0000, 1),
(4255, 'Teleport to Stormwind Illusion', 603, 1954.1400, 21.5220, 239.7180, 1),
(4256, 'Teleport to Sunwell Plateau', 580, 1861.4500, 495.1250, 82.9059, 1),
(4257, 'Teleport to Twin Emps Effect DND', 531, -8971.8100, 1321.4700, -104.2490, 1),
(4258, 'Teleport to Violet Stand', 571, 5724.6200, 1013.1700, 174.4800, 1),
(4259, 'Teleport to the Silvermoon', 530, 10021.1000, -7014.8700, 49.7100, 1),
(4260, 'Teleport to the Undercity', 0, 1805.9300, 335.6600, 70.3900, 1),
(4261, 'Teleport', 1, -3891.8000, -4609.9700, 9.5011, 1),
(4262, 'Teleport', 409, 736.5160, -1176.3500, -119.0060, 1),
(4263, 'Teleport', 531, -8306.6800, 2060.8400, 133.0620, 1),
(4264, 'Teleport', 531, -8330.6300, 2123.1400, 133.0620, 1),
(4265, 'Teleport', 533, 2633.4900, -3529.5600, 274.1110, 1),
(4266, 'Teleport', 533, 2905.6300, -3769.9600, 273.6200, 1),
(4267, 'Teleport', 571, 3574.2200, 6652.1300, 195.1850, 1),
(4268, 'Teleport', 571, 3646.7400, 5893.2000, 174.4830, 1),
(4269, 'Teleport', 571, 4590.9400, -5711.2400, 184.5070, 1),
(4270, 'Teleport', 576, 504.7420, 88.9122, -16.1245, 1),
(4271, 'Teleport', 578, 1103.4700, 1049.5700, 512.0000, 1),
(4272, 'Teleport', 600, -369.0000, -601.0000, 2.0000, 1),
(4273, 'Teleport: Argent Tournament', 571, 8480.5500, 1092.9000, 554.4850, 1),
(4274, 'Teleport: Black Temple', 530, -3560.5200, 583.3530, 10.9431, 1),
(4275, 'Teleport: Darnassus', 1, 9664.1400, 2526.3600, 1332.6900, 1),
(4276, 'Teleport: Moonglade', 1, 7992.6200, -2680.0400, 512.0990, 1),
(4277, 'Teleport: Shattrath', 530, -1842.0700, 5497.1700, -12.4306, 1),
(4278, 'Teleport: Stonard', 0, -10469.0000, -3331.5400, 25.4716, 1),
(4279, 'Teleport: Theramore', 1, -3748.1100, -4440.2100, 30.5688, 1),
(4280, 'Tempest Keep', 550, 411.4090, -39.8267, 20.1802, 1);
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
(4281, 'Tempest Keep: The Arcatraz', 552, 278.6480, -12.6903, 22.4479, 1),
(4282, 'Tempest Keep: The Botanica', 553, 17.5470, 404.8610, -27.1645, 1),
(4283, 'Tempest Keep: The Mechanar', 554, 169.2420, -12.2941, -0.0010, 1),
(4284, 'Terokkar Forest Skettis', 530, -3944.9300, 3664.0900, 287.9900, 1),
(4285, 'Terokkar Forest Terokks Rest', 530, -3868.3500, 3521.8800, 278.5610, 1),
(4286, 'Test of Lore', 1, -2354.0300, -1902.0700, 95.7800, 1),
(4287, 'The Barrens Baeldun Keep', 1, -4071.7300, -2381.6400, 126.2140, 1),
(4288, 'The Barrens Dreadmist Den', 1, 318.8870, -2225.7300, 212.5040, 1),
(4289, 'The Chilled Quagmire spiritguide', 571, 5103.1300, 3462.1300, 368.5680, 1),
(4290, 'The Chilled Quagmire spirithealer', 571, 5099.0300, 3469.6700, 368.4850, 1),
(4291, 'The Nexus', 576, 545.9580, -125.9330, -24.9367, 1),
(4292, 'The Skybreaker', 623, 5.2403, 0.2579, 20.8691, 1),
(4293, 'The Storm Peaks Frostfloe Deep', 571, 8300.4300, -2564.8600, 1153.5900, 1),
(4294, 'The Storm Peaks Frostgrips Hollow', 571, 6956.1400, -2.5143, 808.5300, 1),
(4295, 'The Storm Peaks Gimoraks Den', 571, 7920.2400, -1625.2100, 910.8520, 1),
(4296, 'The Storm Peaks Hibernal Cavern', 571, 7241.0300, -2075.5900, 763.0780, 1),
(4297, 'The Storm Peaks Plain of Echoes', 571, 8109.8900, -2815.7900, 1135.3200, 1),
(4298, 'The Storm Peaks The Forlorn Mine', 571, 6929.4000, -1315.7700, 831.3460, 1),
(4299, 'The Storm Peaks The Frozen Mine', 571, 7768.5000, -16.3215, 864.4010, 1),
(4300, 'The Storm Peaks', 571, 7268.5100, -2125.3800, 778.1580, 1),
(4301, 'The Sunken Ring spiritguide', 571, 5104.7500, 2300.9500, 368.5680, 1),
(4302, 'The Zephyrdock', 1, -1026.8600, 358.4000, 133.3400, 1),
(4303, 'The Zephyrdock', 1, 1139.7300, -4142.9300, 52.0080, 1),
(4304, 'Thousand Needles Darkcloud Pinnacle', 1, -4904.6700, -1970.1300, 86.8811, 1),
(4305, 'Thousand Needles Roguefeather Den', 1, -5533.0400, -1602.8800, 29.1719, 1),
(4306, 'Thousand Needles Splithoof Hold', 1, -4952.9400, -2337.3000, -56.5321, 1),
(4307, 'Thousand Needles The Weathered Nook', 1, -5217.6500, -2788.9900, -7.4459, 1),
(4308, 'Thrall', 0, 1953.8400, 233.8350, 41.8800, 1),
(4309, 'Thrall', 1, 1920.0100, -4123.9500, 43.6300, 1),
(4310, 'Thunderbrew Distillery innkeeper', 0, -5601.6000, -531.2030, 399.7370, 1),
(4311, 'Tirisfal Glades Agamand Family Crypt', 0, 3043.6500, 681.8670, 67.0126, 1),
(4312, 'Tirisfal Glades Gallows End Tavern', 0, 2262.2600, 244.2570, 33.7170, 1),
(4313, 'To Icecrown Airship - Player - Aura - Teleport to Dalaran Trigger', 571, 5831.5300, 497.0880, 657.4660, 1),
(4314, 'Toshleys Station Transporter', 530, 2054.0500, 5569.1600, 263.5710, 1),
(4315, 'Translocate', 0, 1805.0000, 327.0000, 70.5000, 1),
(4316, 'Translocate', 530, -2259.7400, 3215.0300, -4.0500, 1),
(4317, 'Translocate', 530, -2307.3500, 3123.9200, 13.6900, 1),
(4318, 'Translocate', 530, -589.0000, 4079.0000, 143.3000, 1),
(4319, 'Translocate', 530, 12780.9000, -6877.5000, 22.7861, 1),
(4320, 'Translocate', 530, 9334.5000, -7880.7600, 74.9094, 1),
(4321, 'Translocation', 530, -594.0000, 4079.0000, 94.0000, 1),
(4322, 'Trespasser!', 571, 5773.0000, 703.5000, 641.6000, 1),
(4323, 'Trespasser!', 571, 5846.5000, 605.5000, 650.9000, 1),
(4324, 'Trespasser!', 571, 8460.0000, 700.0000, 547.4000, 1),
(4325, 'Trespasser!', 571, 8573.0000, 703.9000, 547.3000, 1),
(4326, 'Turtle (Green Island)dock', 571, 2649.6000, 844.8000, 1.7773, 1),
(4327, 'Turtle (Walker of Waves)dock', 571, 2638.1300, 938.4000, 1.7773, 1),
(4328, 'Uldaman', 70, -42.3058, 263.8750, -48.9355, 1),
(4329, 'Ulduar spirithealer', 571, 9025.6600, -1178.5700, 1060.0800, 1),
(4330, 'Undervatorentry', 0, 1552.8000, 240.7730, 55.4904, 1),
(4331, 'Undervatorentry', 0, 1596.1500, 283.2000, 55.4904, 1),
(4332, 'Undervatorentry', 0, 1596.2000, 197.1040, 55.4904, 1),
(4333, 'Undervatorentryentry', 0, 1564.0000, 240.6560, 55.7571, 1),
(4334, 'Undervatorentryentry', 0, 1595.3800, 197.7060, 55.4904, 1),
(4335, 'Undervatorentryentry', 0, 1595.3800, 213.3330, 57.8905, 1),
(4336, 'Undervatorentryentry', 0, 1595.6500, 271.8890, 55.7571, 1),
(4337, 'Use Legion Teleporter', 530, -2833.0900, 1949.8900, 201.2560, 1),
(4338, 'Vampiric Shadowbat', 532, -10930.9000, -1995.7500, 49.4768, 1),
(4339, 'Vanish', 309, -11516.1000, -1605.3100, 41.3000, 1),
(4340, 'Vator2entry', 90, -800.3290, 314.9250, -272.4900, 1),
(4341, 'Vatorentry', 0, -5163.7700, 655.3050, 348.5880, 1),
(4342, 'Vatorentry', 0, -5164.2400, 650.3540, 247.9780, 1),
(4343, 'Vortex', 616, 755.0000, 1301.0000, 280.0000, 1),
(4344, 'Watery Grave', 548, 337.6900, -732.8700, -13.7400, 1),
(4345, 'Watery Grave', 548, 365.5300, -737.1200, -14.0000, 1),
(4346, 'Watery Grave', 548, 366.2700, -709.4000, -13.9200, 1),
(4347, 'Watery Grave', 548, 372.8500, -690.8400, -13.9100, 1),
(4348, 'Weegli Blastfuse', 209, 1881.0500, 1297.3600, 48.4190, 1),
(4349, 'Western Plaguelands Mardenholde Keep', 0, 2936.4100, -1395.9000, 166.0270, 1),
(4350, 'Westfall The Cooper Residence', 0, -11023.1000, 1547.4800, 44.4936, 1),
(4351, 'Winterspring Everlook', 1, 6768.3400, -4668.5200, 723.7480, 1),
(4352, 'Winterspring Moon Horror Den', 1, 7124.2700, -4637.7800, 639.6550, 1),
(4353, 'Wyrmrest Temple flightMaster', 571, 3647.2600, 244.0510, 52.3397, 1),
(4354, 'Wyrmrest Temple spirithealer', 571, 3546.8600, 272.9880, 45.5817, 1),
(4355, 'Zeppelin (The Iron Eagle)dock', 0, -12452.0000, 221.0670, 31.7681, 1),
(4356, 'Zeppelin (The Iron Eagle)dock', 1, 1359.5700, -4632.2000, 53.6569, 1),
(4357, 'Zeppelin (The Purple Princess)dock', 0, -12407.5000, 211.8380, 31.5014, 1),
(4358, 'Zeppelin (The Purple Princess)dock', 0, 2061.4200, 235.5580, 100.1520, 1),
(4359, 'Zeppelin (The Thundercaller)dock', 0, 2064.2400, 291.6030, 97.0904, 1),
(4360, 'Zeppelin (The Thundercaller)dock', 1, 1319.3200, -4658.0900, 53.8358, 1),
(4361, 'Zeppelindock', 571, 1412.9900, -3092.5800, 166.2270, 1),
(4362, 'ZulDrak Amphitheater of Anguish', 571, 5715.7900, -2945.0100, 296.5510, 1),
(4363, 'ZulDrak ZimTorga', 571, 5756.5800, -3567.6300, 387.0390, 1),
(4364, '[PH] Teleport to Auberdine', 1, 6581.0500, 767.5000, 5.7843, 1),
(4365, '[PH] Teleport to Booty Bay', 0, -14457.0000, 496.4500, 39.1392, 1),
(4366, '[PH] Teleport to Felwood', 1, 5483.9000, -749.8810, 334.6210, 1),
(4367, '[PH] Teleport to GromGol', 0, -12415.0000, 207.6180, 31.5017, 1),
(4368, '[PH] Teleport to Menethil Harbor', 0, -3752.8100, -851.5580, 10.1153, 1),
(4369, '[PH] Teleport to Orgrimmar', 1, 1552.5000, -4420.6600, 8.9480, 1),
(4370, '[PH] Teleport to Theramore', 1, -3615.4900, -4467.3400, 21.6032, 1),
(4371, 'c-Maraudon', 1, -1424.0400, 2945.0100, 134.5400, 1),
(4372, 'c-Tauren start', 1, -3034.7000, 144.0400, 70.8700, 1),
(4373, 'c1-Blackfathom Deeps', 1, 4158.0100, 877.6000, -20.6800, 1),
(4374, 'c1-Blackrock Mountain', 0, -7502.2000, -1152.9800, 269.5500, 1),
(4375, 'c1-Coilfang', 530, 571.1000, 6938.9700, -16.8100, 1),
(4376, 'c1-Deadmine exit', 0, -11367.5000, 1617.1000, 71.2200, 1),
(4377, 'c1-Dire Maul', 1, -3626.3900, 917.3700, 150.1300, 1),
(4378, 'c1-Ebon Hold', 609, 2390.0200, -5640.9100, 377.0900, 1),
(4379, 'c1-Sunken Temple', 0, -10416.5000, -3832.5300, -36.9200, 1),
(4380, 'c1-The Noxious Pass', 609, 2528.2200, -5580.4400, 162.0200, 1),
(4381, 'c1-Timbermaw Hold', 1, 7016.7500, -2153.8400, 595.0900, 1),
(4382, 'c1-Wailing Caverns', 1, -588.5300, -2037.6900, 57.6000, 1),
(4383, 'c2-Blackfathom Deeps', 1, 4156.6000, 909.8900, -20.9700, 1),
(4384, 'c2-Blackrock Mountain', 0, -7591.3100, -1114.4400, 249.9100, 1),
(4385, 'c2-Coilfang', 530, 571.1000, 6938.9700, -15.2000, 1),
(4386, 'c2-Deadmine exit', 0, -11367.1000, 1610.4800, 76.6300, 1),
(4387, 'c2-Dire Maul', 1, -3628.0800, 919.5500, 137.8400, 1),
(4388, 'c2-Ebon Hold', 609, 2383.6500, -5645.2000, 420.7700, 1),
(4389, 'c2-Sunken Temple', 0, -10408.7000, -3834.2900, -44.6900, 1),
(4390, 'c2-The Noxious Pass', 609, 2538.3500, -5573.0500, 162.4600, 1),
(4391, 'c3-Blackfathom Deeps', 1, 4157.4600, 916.4400, -17.4000, 1),
(4392, 'c3-Coilfang', 530, 651.0700, 6865.3700, -82.3400, 1),
(4393, 'c3-Deadmine exit', 0, -11381.5000, 1584.1100, 82.1000, 1),
(4394, 'c3-Ebon Hold', 609, 2325.0300, -5659.6000, 382.2400, 1),
(4395, 'c3-Scarlet Enclave', 609, 2409.0900, -5722.3700, 154.0000, 1),
(4396, 'c3-Sunken Temple', 0, -10325.2000, -3865.8600, -44.4500, 1),
(4397, 'c3-The Noxious Pass', 609, 2546.6100, -5563.8900, 162.8800, 1),
(4398, 'c4-Coilfang', 530, 607.1700, 6908.6800, -49.2000, 1),
(4399, 'c4-Deadmine exit', 0, -11379.6000, 1578.7900, 87.8400, 1),
(4400, 'c4-Ebon Hold', 609, 2348.5800, -5695.3500, 382.2400, 1),
(4401, 'c4-Scarlet Enclave', 609, 2402.8600, -5727.0300, 154.0000, 1),
(4402, 'c4-The Noxious Pass', 609, 2563.5200, -5547.7900, 163.2700, 1),
(4403, 'c5-Coilfang', 530, 574.6800, 6942.9300, -37.7200, 1),
(4404, 'c5-Deadmine exit', 0, -11340.2000, 1571.6100, 94.4400, 1),
(4405, 'c6-Coilfang', 530, 723.7400, 6865.7800, -74.1000, 1),
(4406, 'c7-Coilfang', 530, 731.5700, 6866.0100, -70.4700, 1);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
-- Manual travelnode coverage for the Aldrassil ramp in Shadowglen
-- (Teldrassil, map 1, zone 141). Adds 9 anchor nodes along the spiral
-- ramp (base -> intermediate ramp waypoints -> top platform near
-- Tenaron Stormgrip). All nodes are `linked = 0` so
-- `.playerbots travel generatenode` will iterate them and let mmap
-- compute the actual walk paths between consecutive nodes. Splitting
-- the climb into short segments (~30y each) gives mmap a much better
-- chance of resolving each piece than a single 300y end-to-end probe.
--
-- TEMPORARILY DISABLED: isolating generatenode behaviour on the OG
-- (cmangos-imported) graph. Re-enable by removing the comment prefix
-- once the regen flow is verified stable.
-- SET @n1 := (SELECT IFNULL(MAX(id), 0) + 1 FROM playerbots_travelnode);
-- SET @n2 := @n1 + 1;
-- SET @n3 := @n1 + 2;
-- SET @n4 := @n1 + 3;
-- SET @n5 := @n1 + 4;
-- SET @n6 := @n1 + 5;
-- SET @n7 := @n1 + 6;
-- SET @n8 := @n1 + 7;
-- SET @n9 := @n1 + 8;
-- INSERT INTO playerbots_travelnode (id, name, map_id, x, y, z, linked) VALUES
-- (@n1, 'Aldrassil Ramp 1 (base)', 1, 10413.756, 887.97363, 1319.3668, 0),
-- (@n2, 'Aldrassil Ramp 2', 1, 10440.520, 870.32320, 1328.9324, 0),
-- (@n3, 'Aldrassil Ramp 3', 1, 10497.001, 854.46014, 1345.1770, 0),
-- (@n4, 'Aldrassil Ramp 4', 1, 10517.199, 821.48640, 1354.7914, 0),
-- (@n5, 'Aldrassil Ramp 5', 1, 10477.926, 847.88855, 1372.1685, 0),
-- (@n6, 'Aldrassil Ramp 6', 1, 10455.358, 831.34240, 1380.9377, 0),
-- (@n7, 'Aldrassil Ramp 7', 1, 10460.220, 800.71716, 1388.3368, 0),
-- (@n8, 'Aldrassil Ramp 8', 1, 10507.434, 793.30420, 1397.2166, 0),
-- (@n9, 'Aldrassil Ramp 9 (top)', 1, 10495.496, 804.67700, 1397.2662, 0);
SELECT 1; -- no-op so the file is still valid SQL for the updater

351
extract_ac_client_data.sh Normal file
View File

@ -0,0 +1,351 @@
#!/usr/bin/env bash
# =============================================================================
# AzerothCore client-data extraction for mod-playerbots.
# Run from anywhere — output lands in $SERVER_DATA_DIR.
# =============================================================================
set -euo pipefail
# ─── PATHS ──────────────────────────────────────────────────────────────────
WOW_CLIENT_DATA="/home/dev/wow_client_data"
TOOLS_DIR="/home/dev/azerothcore_installer/_server/azerothcore/env/dist/bin"
SERVER_DATA_DIR="$TOOLS_DIR"
# ─── TOGGLES ────────────────────────────────────────────────────────────────
EXTRACT_DBC_AND_MAPS=true
EXTRACT_VMAPS=true
EXTRACT_MMAPS=true
MMAP_THREADS=0 # 0 = auto-detect (each thread uses 1-2 GB RAM)
MMAP_SINGLE_MAP="" # e.g. "489" for Warsong Gulch only
# Verbatim copy of azerothcore-wotlk/master:src/tools/mmaps_generator/mmaps-config.yaml
# (also the same config the mod-playerbots fork ships).
MMAPS_CONFIG_YAML=$(cat <<'YAML_EOF'
mmapsConfig:
skipLiquid: false
skipContinents: false
skipJunkMaps: true
skipBattlegrounds: false
# Path to the directory containing navigation data files.
# This directory should contain the "maps" and "vmaps" folders,
# and is also where the "mmaps" folder will be created or located.
dataDir: "./"
meshSettings:
# Here we have global config for recast navigation.
# It's possible to override these data on map or tile level (see mapsOverrides).
# Maximum slope angle (in degrees) NPCs can walk on.
# Surfaces steeper than this will be considered unwalkable.
walkableSlopeAngle: 50
# --- Cell Size Calculation ---
# Many parameters below are defined in "cell units".
# In RecastDemo, you often work with world units instead of cell units.
# The actual generator uses (src/tools/mmaps_generator/Config.cpp:28):
#
# cellSize = MMAP::GRID_SIZE / vertexPerMapEdge
#
# Where:
# MMAP::GRID_SIZE = 533.3333f (the size of one map tile in world units)
# vertexPerMapEdge = number of vertices along one edge of the full map grid
#
# Example (AC stock):
# vertexPerMapEdge = 2000 → cellSize ≈ 533.3333 / 2000 ≈ 0.2667 yd
#
# IMPORTANT: when changing vertexPerMapEdge, the per-cell parameters
# below (walkableHeight, walkableClimb, walkableRadius) must be re-scaled
# to preserve their world-unit semantics. Doubling vertexPerMapEdge
# halves cellSize, so cell counts must double to keep the same yd value.
#
# To convert a value from cell units to world units (e.g., walkableClimb),
# multiply by cellSize. For example, a walkableClimb of 6 at 2000 resolution:
# 6 × 0.2667 ≈ 1.60 yd
# Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
# Controls how much vertical clearance is required.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 6 cells × 0.2667 yd ≈ 1.60 yd — matches WoW player capsule height at
# 2000 resolution (AC stock). Preserves the 1.60 yd world-unit
# ceiling-clearance requirement.
walkableHeight: 6
# Maximum height difference (in cell units) NPCs can step up or down.
# Higher values allow walking over fences, ledges, or steps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
#
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
# Classic WotLK uses 4, which forces creatures to walk around fences.
# 6 cells × 0.2667 yd ≈ 1.60 yd — Vanilla-WotLK step semantics at
# 2000 resolution. Preserves the 1.60 yd world-unit step. The mmap
# is shared with every creature, NPC patrol, escort, and quest mob;
# tightening below stock breaks patrols that cross 1.5y ledges.
walkableClimb: 4
# Minimum distance (in cell units) around walkable surfaces.
# Helps prevent NPCs from clipping into walls and narrow gaps.
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
# 2 cells × 0.2667 yd ≈ 0.53 yd — AC stock world-unit buffer at
# 2000 resolution. Tested wider (= 0.71y world units): erodes polys
# near mountains/cliffs so pathfinder routes through surviving
# (higher/worse) polys → bot climbs mountains.
walkableRadius: 2
# Number of vertices along one edge of the entire map's navmesh grid.
# Higher values increase mesh resolution but also CPU/memory usage.
# 2000 = AC stock baseline. cellSize ≈ 0.2667 yd.
vertexPerMapEdge: 2000
# Number of vertices along one edge of each tile chunk.
# Must divide vertexPerMapEdge evenly — the generator uses integer
# division: tilesPerMapEdge = vertexPerMap / vertexPerTile
# (src/tools/mmaps_generator/Config.cpp:144).
# A higher vertex count per tile means fewer total tiles,
# reducing runtime work to load, unload, and manage tiles.
# 80 = AC stock baseline. 2000 / 80 = 25 tiles per map edge, 625
# tiles per map (~21y per tile). Lots of small tiles, low per-tile
# RAM, more seams to stitch across.
vertexPerTileEdge: 80
# Tolerance for how much a polygon can deviate from the original geometry when simplified.
# Higher values produce simpler (faster) meshes but can reduce accuracy.
# 0.8 (vs the AC stock 1.8 and recast canonical 1.3) keeps polygon
# edges close to real terrain. Targets "merged step into ramp"
# simplification artifacts that produce corner-cuts and false NOPATH.
maxSimplificationError: 0.8
# You can override any global parameter for a specific map by specifying its map ID.
# Inside each map override, you can also override parameters per individual tile,
# identified by a string "tileX,tileY" (coordinates).
#
# Overrides cascade: global settings → map overrides → tile overrides.
# For example:
#
# mapsOverrides:
# "0": # Map ID 0 overrides
# walkableRadius: 5 # Override global climb height for entire map 0
#
# tilesOverrides:
# "50,70": # Tile at coordinates (50,70) on map 0
# walkableSlopeAngle: 70 # Override slope angle locally just here
# walkableClimb: 4 # Also override climb height for this tile only
#
# "51,71":
# walkableClimb: 3 # Override climb height for tile (51,71)
#
# "48,32":
# walkableClimb: 1 # Even smaller climb for tile (48,32)
#
# "1": # Map ID 1 overrides example
# walkableHeight: 8 # Increase clearance for whole map 1
#
# tilesOverrides:
# "100,100":
# maxSimplificationError: 2.5 # Looser mesh simplification for this tile only
#
# "101,101":
# walkableRadius: 1 # Smaller NPC radius here for tight corridors
#
# This approach allows very fine-grained control of navigation mesh parameters
# on a per-map and per-tile basis, optimizing pathfinding quality and performance.
#
# All parameters defined globally are eligible for override.
# Just specify the parameter name and new value in the override section.
mapsOverrides:
"562": # Blade's Edge Arena
walkableRadius: 0 # This allows walking on the ropes to the pillars
"48": # Blackfathom Deeps
cellSizeVertical: 0.5334 # ch*2 = 0.2667 * 2 ≈ 0.5334. Reduce the chance to have underground levels.
"529": # Arathi Basin
tilesOverrides:
"30,29": # Lumber Mill
# Make sure that Fear will not drop players rom cliff -
# https://github.com/azerothcore/azerothcore-wotlk/pull/22462#issuecomment-3067024680
walkableSlopeAngle: 45
"530": # Outland
tilesOverrides:
"32,30": # Dark portal
walkableSlopeAngle: 45 # https://github.com/chromiecraft/chromiecraft/issues/8404#issuecomment-3476012660
# debugOutput generates debug files in the `meshes` directory for use with RecastDemo.
# This is useful for inspecting and debugging mmap generation visually.
#
# My workflow:
# 1. Install RecastDemo. I'm building it from the source of this fork: https://github.com/jackpoz/recastnavigation
# 2. In-game, move your character to the area you want to debug.
# 3. Type `.mmap loc` in chat. This will output:
# - The current tile file name (e.g., `04832.mmtile`)
# - The Recast config values used to generate that tile
# 4. Enable `debugOutput` and regenerate mmaps (preferably just the tile from step 3).
# - To regenerate only one tile, delete it from the `mmaps` folder.
# 5. After generation, you will find debug files in the `meshes` folder, including an OBJ file (e.g., `map0004832.obj`)
# 6. Copy these debug files to the `Meshes` folder used by RecastDemo.
# - RecastDemo expects this folder to be in the same directory as its executable.
# 7. In RecastDemo:
# - Click "Input Mesh" and select the `.obj` file
# - Choose "Solo Mesh" in the Sample selector
# 8. (Optional) Reuse the Recast config values from step 3:
# - `cellSizeHorizontal` → "Cell Size"
# - `walkableSlopeAngle` → "Max Slope"
# - `walkableClimb` → "Max Climb"
# - and so on
# 9. Scroll to the bottom of RecastDemo UI and press "Build" to generate the navigation mesh
debugOutput: false
YAML_EOF
)
# =============================================================================
# ─── DO NOT EDIT BELOW ──────────────────────────────────────────────────────
# =============================================================================
[ -n "$SERVER_DATA_DIR" ] || { echo "SERVER_DATA_DIR is not set"; exit 1; }
[ -n "$WOW_CLIENT_DATA" ] || { echo "WOW_CLIENT_DATA is not set"; exit 1; }
mkdir -p "$SERVER_DATA_DIR"
cd "$SERVER_DATA_DIR"
# ─── SAFETY: source MPQs are READ-ONLY to this script ──────────────────────
# Resolve both paths to canonical form and refuse to run if the output dir
# is inside the source. Combined with safe_rm() below, this script cannot
# touch any file inside WOW_CLIENT_DATA.
SERVER_DATA_DIR_REAL="$(cd "$SERVER_DATA_DIR" && pwd -P)"
WOW_CLIENT_DATA_REAL="$(cd "$WOW_CLIENT_DATA" && pwd -P 2>/dev/null || echo "$WOW_CLIENT_DATA")"
case "$SERVER_DATA_DIR_REAL/" in
"$WOW_CLIENT_DATA_REAL"/|"$WOW_CLIENT_DATA_REAL"/*)
echo "ERROR: SERVER_DATA_DIR ($SERVER_DATA_DIR_REAL) is inside WOW_CLIENT_DATA — refusing." >&2
exit 1
;;
esac
# Refuses to remove anything outside SERVER_DATA_DIR. Resolves the parent
# to absolute path so a symlink inside cwd can't trick us into traversing
# into the source. Use this for every cleanup in this script.
safe_rm() {
local target="$1"
local parent_abs base
parent_abs="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)" || return 0
base="$(basename -- "$target")"
local abs="$parent_abs/$base"
case "$abs/" in
"$SERVER_DATA_DIR_REAL"/|"$SERVER_DATA_DIR_REAL"/*) ;;
*)
echo "REFUSING to rm path outside SERVER_DATA_DIR: $target$abs" >&2
exit 1 ;;
esac
rm -rf -- "$target"
}
[ "$MMAP_THREADS" -eq 0 ] && MMAP_THREADS=$(nproc 2>/dev/null || echo 4)
echo "Working dir : $(pwd)"
echo "Tools dir : $TOOLS_DIR"
echo "Threads : $MMAP_THREADS"
echo "Steps : maps=$EXTRACT_DBC_AND_MAPS vmaps=$EXTRACT_VMAPS mmaps=$EXTRACT_MMAPS"
echo
# ─── Symlink Data/ → MPQ source (only when extracting from client) ──────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ] || [ "$EXTRACT_VMAPS" = true ]; then
has_mpqs() { find "$1" -maxdepth 1 -iname "*.mpq" -print -quit 2>/dev/null | grep -q .; }
if has_mpqs "$WOW_CLIENT_DATA"; then
MPQ_DIR="$WOW_CLIENT_DATA"
elif has_mpqs "$WOW_CLIENT_DATA/Data"; then
MPQ_DIR="$WOW_CLIENT_DATA/Data"
else
echo "ERROR: no .mpq files in $WOW_CLIENT_DATA" >&2
exit 1
fi
MPQ_DIR="$(cd "$MPQ_DIR" && pwd)"
# Symlink only — refuse to clobber an existing real directory.
if [ -e Data ] && [ ! -L Data ]; then
echo "ERROR: Data/ exists in $(pwd) but is not a symlink" >&2
exit 1
fi
ln -sfn "$MPQ_DIR" Data
echo "Data/ → $MPQ_DIR"
fi
# ─── STEP 1: DBCs + Maps ────────────────────────────────────────────────────
if [ "$EXTRACT_DBC_AND_MAPS" = true ]; then
echo
echo "[1/3] Extracting DBCs + Maps..."
# Clean slate — map_extractor refuses to run if these dirs already exist.
safe_rm dbc
safe_rm maps
safe_rm Cameras
# -e 7 = bitfield MAP(1)|DBC(2)|CAMERA(4) — extract everything.
# The old "-e 2" was DBC-only and skipped maps + cameras entirely.
"$TOOLS_DIR/map_extractor" -e 7 -f 0
if [ ! -d maps ] || [ -z "$(ls -A maps 2>/dev/null)" ]; then
echo "ERROR: map_extractor finished but maps/ is empty — check its output above" >&2
exit 1
fi
fi
# ─── STEP 2: VMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_VMAPS" = true ]; then
echo
echo "[2/3] Extracting VMaps..."
# Clean slate — vmap4_extractor refuses to run if these dirs already exist.
safe_rm Buildings
safe_rm vmaps
"$TOOLS_DIR/vmap4_extractor" -l -d ./Data
mkdir -p vmaps
"$TOOLS_DIR/vmap4_assembler" Buildings vmaps
safe_rm Buildings
if [ ! -d vmaps ] || [ -z "$(ls -A vmaps 2>/dev/null)" ]; then
echo "ERROR: vmap4_assembler finished but vmaps/ is empty — check output above" >&2
exit 1
fi
fi
# ─── STEP 3: MMaps ──────────────────────────────────────────────────────────
if [ "$EXTRACT_MMAPS" = true ]; then
if [ ! -d maps ]; then
echo "ERROR: maps/ missing in $(pwd) — run with EXTRACT_DBC_AND_MAPS=true once" >&2
exit 1
fi
if [ ! -d vmaps ]; then
echo "ERROR: vmaps/ missing in $(pwd) — run with EXTRACT_VMAPS=true once" >&2
exit 1
fi
echo
echo "[3/3] Generating MMaps... (do not interrupt)"
printf '%s\n' "$MMAPS_CONFIG_YAML" > mmaps-config.yaml
# Wipe any existing tiles before regenerating. Mixed tiles from
# previous runs (different cellSize / verticesPerTileEdge / etc.)
# would otherwise be silently kept and mixed with new ones,
# producing a corrupt navmesh. Clean slate every mmap run.
safe_rm mmaps
mkdir -p mmaps
# Workaround: some mmaps_generator builds write a few tiles to /mmaps
# via an absolute path. Pre-create it so the writes don't fail, then
# fold the strays into our local mmaps/ at the end.
sudo rm -rf /mmaps
sudo mkdir -p /mmaps && sudo chmod 777 /mmaps
CMD=("$TOOLS_DIR/mmaps_generator" --config mmaps-config.yaml --threads "$MMAP_THREADS")
[ -n "$MMAP_SINGLE_MAP" ] && CMD+=("$MMAP_SINGLE_MAP")
START=$(date +%s)
"${CMD[@]}"
ELAPSED=$(( $(date +%s) - START ))
if compgen -G "/mmaps/*.mmtile" >/dev/null; then
cp /mmaps/*.mmtile mmaps/ && rm -f /mmaps/*.mmtile
fi
echo
echo "MMap done in $((ELAPSED / 60))m $((ELAPSED % 60))s"
echo "Tiles: $(ls mmaps/*.mmtile 2>/dev/null | wc -l)"
fi
echo
echo "Done. Restart worldserver to pick up changes."

View File

@ -173,7 +173,7 @@ std::vector<uint32> const vFlagsIC = {GO_HORDE_BANNER,
GO_HORDE_BANNER_GRAVEYARD_H, GO_HORDE_BANNER_GRAVEYARD_H,
GO_HORDE_BANNER_GRAVEYARD_H_CONT}; GO_HORDE_BANNER_GRAVEYARD_H_CONT};
// BG Waypoints (vmangos) // BG Waypoints
// Horde Flag Room to Horde Graveyard // Horde Flag Room to Horde Graveyard
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = { BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {

View File

@ -6,10 +6,10 @@
#include "CheckValuesAction.h" #include "CheckValuesAction.h"
#include "Event.h" #include "Event.h"
#include "ObjectGuid.h"
#include "ServerFacade.h" #include "ServerFacade.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "TravelNode.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {} CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/)
botAI->Ping(bot->GetPositionX(), bot->GetPositionY()); botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
} }
if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT))
{
TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT));
}
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets"); GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
GuidVector all_targets = *context->GetValue<GuidVector>("all targets"); GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs"); GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");

View File

@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event)
return false; return false;
std::vector<WorldPosition> beginPath, endPath; std::vector<WorldPosition> beginPath, endPath;
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot); TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot);
std::ostringstream out; std::ostringstream out;
out << "Traveling to " << dest->getTitle() << ": "; out << "Traveling to " << dest->getTitle() << ": ";
@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event)
{ {
WorldPosition pos(bot); WorldPosition pos(bot);
std::string const name = "USER:" + text.substr(9); std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName();
std::string const name = "USER:" + suffix;
/* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal. {
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
TravelNodeMap::instance().addNode(pos, name, false, true);
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000)) for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
{
endNode->setLinked(false); endNode->setLinked(false);
} }
botAI->TellMasterNoFacing("Node " + name + " created."); botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes.");
TravelNodeMap::instance().setHasToGen();
return true; return true;
} }
@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event)
if (startNode->isImportant()) if (startNode->isImportant())
{ {
botAI->TellMasterNoFacing("Node can not be removed."); botAI->TellMasterNoFacing("Node can not be removed.");
return true;
} }
TravelNodeMap::instance().m_nMapMtx.lock(); {
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
TravelNodeMap::instance().removeNode(startNode); TravelNodeMap::instance().removeNode(startNode);
botAI->TellMasterNoFacing("Node removed."); }
TravelNodeMap::instance().m_nMapMtx.unlock();
TravelNodeMap::instance().setHasToGen(); botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes.");
return true; return true;
} }
@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event)
node->removeLinkTo(path.first, true); node->removeLinkTo(path.first, true);
return true; return true;
} }
else if (text.find("gen node") != std::string::npos) else if (text.find("gen node") != std::string::npos ||
text.find("gen path") != std::string::npos)
{ {
// Pathfinder // Disabled: generateAll() touches Map / grid / mmap state that is only
TravelNodeMap::instance().generateNodes(); // safe to mutate on the world thread. Running it from a detached worker
return true; // (or from a bot tick on a MapUpdater thread) races with world updates
} // and freezes the server. Use the console command instead, which runs
else if (text.find("gen path") != std::string::npos) // synchronously on the world thread:
{ // .playerbots travel generatenode
TravelNodeMap::instance().generatePaths(); botAI->TellMasterNoFacing(
"Disabled in chat. Run '.playerbots travel generatenode' from the server console.");
return true; return true;
} }
else if (text.find("crop path") != std::string::npos) else if (text.find("crop path") != std::string::npos)
@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event)
[] []
{ {
TravelNodeMap::instance().removeNodes(); TravelNodeMap::instance().removeNodes();
TravelNodeMap::instance().loadNodeStore(); TravelNodeMap::instance().LoadNodeStore();
}); });
t.detach(); t.detach();
@ -297,7 +300,7 @@ bool DebugAction::Execute(Event event)
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal. // uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
std::vector<WorldPosition> ppath = l.second->getPath(); std::vector<WorldPosition> ppath = l.second->GetPath();
for (auto p : ppath) for (auto p : ppath)
{ {

View File

@ -29,6 +29,10 @@ void DestroyItemAction::DestroyItem(FindItemVisitor* visitor)
std::vector<Item*> items = visitor->GetResult(); std::vector<Item*> items = visitor->GetResult();
for (Item* item : items) for (Item* item : items)
{ {
// backstop: never drop an active quest item
if (bot->HasQuestForItem(item->GetTemplate()->ItemId))
continue;
std::ostringstream out; std::ostringstream out;
out << chat->FormatItem(item->GetTemplate()) << " destroyed"; out << chat->FormatItem(item->GetTemplate()) << " destroyed";
botAI->TellMaster(out); botAI->TellMaster(out);
@ -67,18 +71,11 @@ bool SmartDestroyItemAction::Execute(Event /*event*/)
return true; return true;
} }
// ITEM_USAGE_QUEST is excluded — those are still-needed quest items
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless. std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
if (!AI_VALUE(bool, "can sell") &&
AI_VALUE(
bool,
"should get money")) // We need money so quest items are less important since they can't directly be sold.
bestToDestroy.push_back(ITEM_USAGE_QUEST);
else // We don't need money so destroy the cheapest stuff.
{
bestToDestroy.push_back(ITEM_USAGE_VENDOR); bestToDestroy.push_back(ITEM_USAGE_VENDOR);
bestToDestroy.push_back(ITEM_USAGE_AH); bestToDestroy.push_back(ITEM_USAGE_AH);
}
// If we still need room // If we still need room
bestToDestroy.push_back( bestToDestroy.push_back(

View File

@ -19,83 +19,8 @@
#include "Transport.h" #include "Transport.h"
#include "Map.h" #include "Map.h"
namespace // Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport,
{ // BoardTransport) are now on MovementAction — inherited by FollowAction.
Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
{
if (!map || !ref)
return nullptr;
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
for (float const pz : probes)
{
if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref))
return t;
}
return nullptr;
}
// Attempts to find a point on the leader's transport that is closer to the bot,
// by probing along the segment from master -> bot and returning the last point
// that is still detected as being on the expected transport.
bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
float masterX, float masterY, float masterZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ)
{
if (!map || !expectedTransport || !ref)
return false;
uint32 const phaseMask = ref->GetPhaseMask();
// Ensure master is actually detected on that transport (tolerant).
if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport)
return false;
// The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z.
float const probeZ = std::max(masterZ, botZ);
// Adaptive step count: small platforms need tighter sampling.
float const dx2 = botX - masterX;
float const dy2 = botY - masterY;
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
float const dx = (botX - masterX) / static_cast<float>(steps);
float const dy = (botY - masterY) / static_cast<float>(steps);
// Master must actually be on the expected transport for this to work.
if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport)
return false;
float lastX = masterX;
float lastY = masterY;
bool found = false;
for (int32 i = 1; i <= steps; ++i)
{
float const px = masterX + dx * i;
float const py = masterY + dy * i;
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
if (t != expectedTransport)
break;
lastX = px;
lastY = py;
found = true;
}
if (!found)
return false;
outX = lastX;
outY = lastY;
outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat
return true;
}
}
bool FollowAction::Execute(Event /*event*/) bool FollowAction::Execute(Event /*event*/)
{ {
@ -170,9 +95,8 @@ bool FollowAction::Execute(Event /*event*/)
bool const movingAllowed = IsMovingAllowed(); bool const movingAllowed = IsMovingAllowed();
bool const dupMove = IsDuplicateMove(destX, destY, destZ); bool const dupMove = IsDuplicateMove(destX, destY, destZ);
bool const waiting = IsWaitingForLastMove(priority);
if (movingAllowed && !dupMove && !waiting) if (movingAllowed && !dupMove)
{ {
if (bot->IsSitState()) if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND); bot->SetStandState(UNIT_STAND_STATE_STAND);

View File

@ -24,9 +24,7 @@ using ai::buff::MakeAuraQualifierForBuff;
using ai::spell::HasSpellOrCategoryCooldown; using ai::spell::HasSpellOrCategoryCooldown;
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {}
{
}
bool CastSpellAction::Execute(Event /*event*/) bool CastSpellAction::Execute(Event /*event*/)
{ {
@ -53,18 +51,12 @@ bool CastSpellAction::Execute(Event /*event*/)
wstrToLower(wnamepart); wstrToLower(wnamepart);
if (!Utf8FitTo(spell, wnamepart)) if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue;
if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue; continue;
uint32 itemId = spellInfo->Effects[0].ItemType; uint32 itemId = spellInfo->Effects[0].ItemType;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto) if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue;
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue; continue;
if (spellInfo->Id > castId) if (spellInfo->Id > castId)
@ -92,10 +84,7 @@ bool CastSpellAction::isUseful()
} }
Unit* spellTarget = GetTarget(); Unit* spellTarget = GetTarget();
if (!spellTarget) if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false;
if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false; return false;
// float combatReach = bot->GetCombatReach() + target->GetCombatReach(); // float combatReach = bot->GetCombatReach() + target->GetCombatReach();
@ -143,10 +132,7 @@ CastMeleeSpellAction::CastMeleeSpellAction(
bool CastMeleeSpellAction::isUseful() bool CastMeleeSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target || !bot->IsWithinMeleeRange(target))
return false;
if (!bot->IsWithinMeleeRange(target))
return false; return false;
return CastSpellAction::isUseful(); return CastSpellAction::isUseful();
@ -162,10 +148,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
bool CastMeleeDebuffSpellAction::isUseful() bool CastMeleeDebuffSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target || !bot->IsWithinMeleeRange(target))
return false;
if (!bot->IsWithinMeleeRange(target))
return false; return false;
return CastDebuffSpellAction::isUseful(); return CastDebuffSpellAction::isUseful();
@ -175,14 +158,55 @@ bool CastAuraSpellAction::isUseful()
{ {
if (!GetTarget() || !CastSpellAction::isUseful()) if (!GetTarget() || !CastSpellAction::isUseful())
return false; return false;
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration); Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
if (!aura) if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
if (beforeDuration && aura->GetDuration() < beforeDuration)
return true; return true;
return false; return false;
} }
bool CastBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
return !aura || (beforeDuration && aura->GetDuration() < beforeDuration);
}
bool CastBuffSpellAction::Execute(Event /*event*/)
{
return botAI->CastSpell(spell, GetTarget());
}
bool GroupBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
if (ai::buff::IsGroupVariantEnabled(bot, spell))
{
std::string const groupVariant = ai::buff::GroupVariantFor(spell);
if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration))
return false;
}
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
return false;
}
bool GroupBuffSpellAction::Execute(Event /*event*/)
{
std::string const castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, spell);
return botAI->CastSpell(castName, GetTarget());
}
CastEnchantItemMainHandAction::CastEnchantItemMainHandAction( CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
@ -248,25 +272,16 @@ Value<Unit*>* CurePartyMemberAction::GetTargetValue()
return context->GetValue<Unit*>("party member to dispel", dispelType); return context->GetValue<Unit*>("party member to dispel", dispelType);
} }
// Make Bots Paladin, druid, mage use the greater buff rank spell
// TODO Priest doen't verify il he have components
Value<Unit*>* BuffOnPartyAction::GetTargetValue() Value<Unit*>* BuffOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member without aura", spell);
}
Value<Unit*>* GroupBuffOnPartyAction::GetTargetValue()
{ {
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell)); return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
} }
bool BuffOnPartyAction::Execute(Event /*event*/)
{
std::string castName = spell; // default = mono
auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(
bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
return botAI->CastSpell(castName, GetTarget());
}
// End greater buff fix
CastShootAction::CastShootAction( CastShootAction::CastShootAction(
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0) PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
{ {
@ -365,16 +380,7 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
bool CastEveryManForHimselfAction::isPossible() bool CastEveryManForHimselfAction::isPossible()
{ {
uint32 spellId = AI_VALUE2(uint32, "spell id", spell); uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId) return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
} }
bool CastEveryManForHimselfAction::isUseful() bool CastEveryManForHimselfAction::isUseful()
@ -390,16 +396,7 @@ bool CastEveryManForHimselfAction::isUseful()
bool CastWillOfTheForsakenAction::isPossible() bool CastWillOfTheForsakenAction::isPossible()
{ {
uint32 spellId = AI_VALUE2(uint32, "spell id", spell); uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId) return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
} }
bool CastWillOfTheForsakenAction::isUseful() bool CastWillOfTheForsakenAction::isUseful()
@ -427,10 +424,7 @@ bool UseTrinketAction::Execute(Event /*event*/)
bool UseTrinketAction::UseTrinket(Item* item) bool UseTrinketAction::UseTrinket(Item* item)
{ {
if (bot->CanUseItem(item) != EQUIP_ERR_OK) if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true))
return false;
if (bot->IsNonMeleeSpellCast(true))
return false; return false;
uint8 bagIndex = item->GetBagSlot(); uint8 bagIndex = item->GetBagSlot();
@ -477,14 +471,13 @@ bool UseTrinketAction::UseTrinket(Item* item)
if (spellProcFlag != 0) return false; if (spellProcFlag != 0) return false;
if (!botAI->CanCastSpell(spellId, bot, false)) if (!botAI->CanCastSpell(spellId, bot, false))
{
return false; return false;
}
break; break;
} }
} }
if (!spellId) if (!spellId)
return false; return false;
WorldPacket packet(CMSG_USE_ITEM); WorldPacket packet(CMSG_USE_ITEM);
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
@ -500,9 +493,8 @@ bool CastDebuffSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld()) if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false; return false;
}
return CastAuraSpellAction::isUseful() && return CastAuraSpellAction::isUseful() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
} }

View File

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

View File

@ -124,7 +124,6 @@ bool GoAction::Execute(Event event)
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{ {
PathGenerator path(bot); PathGenerator path(bot);
path.CalculatePath(x, y, z, false); path.CalculatePath(x, y, z, false);
Movement::Vector3 end = path.GetEndPosition(); Movement::Vector3 end = path.GetEndPosition();

View File

@ -5,6 +5,9 @@
#include "LootAction.h" #include "LootAction.h"
#include <limits>
#include "Bag.h"
#include "ChatHelper.h" #include "ChatHelper.h"
#include "Event.h" #include "Event.h"
#include "GuildMgr.h" #include "GuildMgr.h"
@ -76,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
bool result = DoLoot(lootObject); bool result = DoLoot(lootObject);
if (result) if (result)
{ {
AI_VALUE(LootObjectStack*, "available loot")->Remove(lootObject.guid); // MarkCompleted (not Remove) — "add all loot" reads
// "nearest corpses" without a lootable filter, so a plain
// Remove lets the same corpse re-enter the stack on the next
// tick. The completed set blocks re-add for ~5 min.
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(lootObject.guid);
context->GetValue<LootObject>("loot target")->Set(LootObject()); context->GetValue<LootObject>("loot target")->Set(LootObject());
} }
return result; return result;
@ -139,8 +146,9 @@ bool OpenLootAction::DoLoot(LootObject& lootObject)
if (go && (go->GetGoState() != GO_STATE_READY)) if (go && (go->GetGoState() != GO_STATE_READY))
return false; return false;
// This prevents dungeon chests like Tribunal Chest (Halls of Stone) from being ninja'd by the bots // Block event-gated chests (Tribunal Chest, Gunship Armory) but allow
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND)) // wild quest GOs (Moonpetal Lily etc.) when the bot is on the quest.
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !lootObject.isNeededQuestItem)
return false; return false;
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots // This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
@ -377,6 +385,12 @@ bool StoreLootAction::Execute(Event event)
// bot->GetSession()->HandleLootMoneyOpcode(packet); // bot->GetSession()->HandleLootMoneyOpcode(packet);
} }
// one make-room destroy per loot packet — CanStoreNewItem after a junk
// destroy can still report full while CMSG_AUTOSTORE_LOOT_ITEM is
// queued, so a multi-quest-item packet would otherwise destroy more
// junk than necessary
bool destroyedThisPacket = false;
for (uint8 i = 0; i < items; ++i) for (uint8 i = 0; i < items; ++i)
{ {
uint32 itemid; uint32 itemid;
@ -402,7 +416,9 @@ bool StoreLootAction::Execute(Event event)
if (!proto) if (!proto)
continue; continue;
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80) // bags >80%: skip non-stackable junk (quest items exempt)
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80 &&
!bot->HasQuestForItem(itemid))
{ {
uint32 maxStack = proto->GetMaxStackSize(); uint32 maxStack = proto->GetMaxStackSize();
if (maxStack == 1) if (maxStack == 1)
@ -438,6 +454,55 @@ bool StoreLootAction::Execute(Event event)
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot); GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
} }
// bags full + quest item: make room by dropping cheapest junk
if (!destroyedThisPacket && bot->HasQuestForItem(itemid))
{
ItemPosCountVec dest;
InventoryResult can =
bot->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, itemcount);
if (can == EQUIP_ERR_INVENTORY_FULL || can == EQUIP_ERR_BAG_FULL)
{
// picked by usage, not quality — high-level bots have no grays
Item* victim = nullptr;
uint32 minPrice = std::numeric_limits<uint32>::max();
auto consider = [&](uint8 bag, uint8 slot)
{
Item* it = bot->GetItemByPos(bag, slot);
if (!it)
return;
ItemTemplate const* tpl = it->GetTemplate();
if (!tpl)
return;
if (bot->HasQuestForItem(tpl->ItemId))
return;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", tpl->ItemId);
if (usage != ITEM_USAGE_NONE && usage != ITEM_USAGE_VENDOR &&
usage != ITEM_USAGE_BAD_EQUIP && usage != ITEM_USAGE_BROKEN_EQUIP)
return;
if (tpl->SellPrice < minPrice)
{
minPrice = tpl->SellPrice;
victim = it;
}
};
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
consider(INVENTORY_SLOT_BAG_0, slot);
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
Bag* pBag = bot->GetBagByPos(bag);
if (!pBag)
continue;
for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot)
consider(bag, static_cast<uint8>(slot));
}
if (victim)
{
bot->DestroyItem(victim->GetBagSlot(), victim->GetSlot(), true);
destroyedThisPacket = true;
}
}
}
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
*packet << itemindex; *packet << itemindex;
bot->GetSession()->QueuePacket(packet); bot->GetSession()->QueuePacket(packet);
@ -453,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto); BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
} }
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid); AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
// release loot // release loot
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8); WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
#include "Action.h" #include "Action.h"
#include "LastMovementValue.h" #include "LastMovementValue.h"
#include "PathGenerator.h"
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
class Player; class Player;
@ -22,12 +23,33 @@ class Position;
#define ANGLE_90_DEG M_PI_2 #define ANGLE_90_DEG M_PI_2
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f) #define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
// Default acceptable path types for GeneratePath
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
struct PathResult
{
Movement::PointsArray points;
G3D::Vector3 actualEnd;
G3D::Vector3 end;
PathType pathType;
bool reachable;
};
class MovementAction : public Action class MovementAction : public Action
{ {
public: public:
MovementAction(PlayerbotAI* botAI, std::string const name); MovementAction(PlayerbotAI* botAI, std::string const name);
protected: protected:
// Emit a one-line trace describing the imminent movement. No-op
// unless the bot has the "debug move" non-combat strategy.
// Subclasses (e.g. NewRpgBaseAction) may override to append richer
// context such as RPG status and target name. Optional `extra`
// is appended verbatim (use it to attach hop labels like
// "node:Stormwind innkeeper" or fallback reasons).
virtual void EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra = nullptr);
bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance, bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
@ -50,7 +72,6 @@ protected:
void SetNextMovementDelay(float delayMillis); void SetNextMovementDelay(float delayMillis);
bool IsMovingAllowed(WorldObject* target); bool IsMovingAllowed(WorldObject* target);
bool IsDuplicateMove(float x, float y, float z); bool IsDuplicateMove(float x, float y, float z);
bool IsWaitingForLastMove(MovementPriority priority);
bool IsMovingAllowed(); bool IsMovingAllowed();
bool Flee(Unit* target); bool Flee(Unit* target);
void ClearIdleState(); void ClearIdleState();
@ -66,6 +87,30 @@ protected:
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000); bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList); bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
bool ExecuteTravelPlan(TravelPlan& state);
// Transport boarding helpers (shared by FollowAction and travel plan)
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
uint32 phaseMask, float x, float y, float z);
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
WorldObject* ref, float refX, float refY, float refZ,
float botX, float botY, float botZ,
float& outX, float& outY, float& outZ);
bool BoardTransport(Transport* transport);
private:
bool LaunchWalkSpline(TravelPlan& state);
bool MoveToSpline(TravelPlan& state, WorldPosition target);
// Per-segment mmap refinement of a travel-node-graph walk batch.
// The graph stores offline-baked coords whose straight-line
// interpolation may pass through geometry the bot can't actually
// traverse. Returns false if any segment is unwalkable per the
// live navmesh, in which case the caller should abort the plan.
bool RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints);
protected: protected:
struct CheckAngle struct CheckAngle
{ {
@ -74,10 +119,6 @@ protected:
}; };
private: private:
// float SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range = 20.0f, bool
// normal_only = false, float step = 8.0f);
const Movement::PointsArray SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount = 5,
bool normal_only = false, float step = 8.0f);
bool wasMovementRestricted = false; bool wasMovementRestricted = false;
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards); void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
}; };

View File

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

View File

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

View File

@ -11,21 +11,20 @@
bool LootAvailableTrigger::IsActive() bool LootAvailableTrigger::IsActive()
{ {
bool distanceCheck = false; // Strategy is non-combat-only — the engine state separation is the
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT)) // safety net. Don't gate on hostiles-in-sight: that locked out
{ // looting in zones with continuous respawns (e.g. cave farms).
distanceCheck = // If a new enemy aggros mid-loot the combat engine takes over, loot
ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE); // resumes on the next non-combat window.
} if (!AI_VALUE(bool, "has available loot"))
else return false;
{
distanceCheck = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"),
INTERACTION_DISTANCE - 2.0f);
}
// if loot target if empty, always pass distance check // "stay" strategy is restrictive: only loot if corpse is at our feet.
return AI_VALUE(bool, "has available loot") && if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
(distanceCheck || AI_VALUE(GuidVector, "all targets").empty()); return ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
return true;
} }
bool FarFromCurrentLootTrigger::IsActive() bool FarFromCurrentLootTrigger::IsActive()

View File

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

View File

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

View File

@ -47,7 +47,13 @@ public:
{ {
if (allowedGOFlags.empty()) if (allowedGOFlags.empty())
{ {
// questgivers for accept/turn-in; rest for quest progression
// (chests, runes, altars, moonwells, lily piles, …)
allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER); allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_CHEST);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GOOBER);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_SPELL_FOCUS);
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GENERIC);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,24 +7,100 @@
#include "AiFactory.h" #include "AiFactory.h"
#include "Event.h" #include "Event.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinHelper.h" #include "PaladinHelper.h"
#include "PlayerbotAI.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "SharedDefines.h" #include "SharedDefines.h"
#include "../../../../../src/server/scripts/Spells/spell_generic.cpp"
#include "Ai/Base/Util/GenericBuffUtils.h"
#include "Group.h"
#include "ObjectAccessor.h"
using ai::buff::MakeAuraQualifierForBuff; static bool IsBlessingTargetCandidate(Player* bot, Player* player)
// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face).
static inline bool IsTankRole(Player* p)
{ {
if (!p) return false; if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId())
if (p->HasTankSpec()) return false;
if (player->IsGameMaster())
return false;
return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 &&
bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(),
player->GetPositionZ());
}
static bool HasBlessingAura(
PlayerbotAI* botAI, Unit* target, std::initializer_list<char const*> auraNames)
{
for (char const* auraName : auraNames)
{
if (botAI->HasAura(auraName, target))
return true; return true;
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p)) }
return false;
}
static bool IsGreaterBlessingMode(Player* bot)
{
return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup());
}
template <typename Predicate>
static Unit* FindBlessingTarget(
Player* bot, PlayerbotAI* botAI, Predicate&& predicate)
{
std::vector<Player*> masters;
std::vector<Player*> healers;
std::vector<Player*> tanks;
std::vector<Player*> others;
Player* master = botAI->GetMaster();
auto addPlayer = [&](Player* player)
{
if (!IsBlessingTargetCandidate(bot, player))
return;
if (player == master)
masters.push_back(player);
else if (botAI->IsHeal(player))
healers.push_back(player);
else if (botAI->IsTank(player))
tanks.push_back(player);
else
others.push_back(player);
};
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
addPlayer(ref->GetSource());
}
else
{
addPlayer(bot);
}
std::vector<std::vector<Player*>*> orderedLists = {
&masters, &healers, &tanks, &others };
for (std::vector<Player*>* players : orderedLists)
{
for (Player* player : *players)
{
if (predicate(player))
return player;
}
}
return nullptr;
}
static inline bool IsTankRole(Player* player)
{
if (!player)
return false;
if (player->HasTankSpec())
return true;
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player))
{ {
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) || if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) || otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
@ -34,33 +110,36 @@ static inline bool IsTankRole(Player* p)
otherAI->HasStrategy("bear", BOT_STATE_COMBAT)) otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
return true; return true;
} }
return false; return false;
} }
// Added for solo paladin patch : determine if he's the only paladin on party
static inline bool IsOnlyPaladinInGroup(Player* bot) static inline bool IsOnlyPaladinInGroup(Player* bot)
{ {
if (!bot) return false; if (!bot)
Group* g = bot->GetGroup(); return false;
if (!g) return true; // solo
uint32 pals = 0u; Group* group = bot->GetGroup();
for (GroupReference* r = g->GetFirstMember(); r; r = r->next()) if (!group)
return true;
uint32 paladins = 0u;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{ {
Player* p = r->GetSource(); Player* player = ref->GetSource();
if (!p || !p->IsInWorld()) continue; if (!player || !player->IsInWorld()) continue;
if (p->getClass() == CLASS_PALADIN) ++pals; if (player->getClass() == CLASS_PALADIN) ++paladins;
} }
return pals == 1u;
return paladins == 1u;
} }
inline std::string const GetActualBlessingOfMight(Unit* target) inline std::string const GetActualBlessingOfMight(Unit* target)
{ {
if (!target->ToPlayer()) if (!target->ToPlayer())
{
return "blessing of might"; return "blessing of might";
}
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass()) switch (target->getClass())
{ {
case CLASS_MAGE: case CLASS_MAGE:
@ -70,21 +149,15 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
break; break;
case CLASS_SHAMAN: case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION) if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
{
return "blessing of wisdom"; return "blessing of wisdom";
}
break; break;
case CLASS_DRUID: case CLASS_DRUID:
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE) if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
{
return "blessing of wisdom"; return "blessing of wisdom";
}
break; break;
case CLASS_PALADIN: case CLASS_PALADIN:
if (tab == PALADIN_TAB_HOLY) if (tab == PALADIN_TAB_HOLY)
{
return "blessing of wisdom"; return "blessing of wisdom";
}
break; break;
} }
@ -94,10 +167,9 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
inline std::string const GetActualBlessingOfWisdom(Unit* target) inline std::string const GetActualBlessingOfWisdom(Unit* target)
{ {
if (!target->ToPlayer()) if (!target->ToPlayer())
{
return "blessing of might"; return "blessing of might";
}
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass()) switch (target->getClass())
{ {
case CLASS_WARRIOR: case CLASS_WARRIOR:
@ -108,21 +180,15 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
break; break;
case CLASS_SHAMAN: case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT) if (tab == SHAMAN_TAB_ENHANCEMENT)
{
return "blessing of might"; return "blessing of might";
}
break; break;
case CLASS_DRUID: case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL) if (tab == DRUID_TAB_FERAL)
{
return "blessing of might"; return "blessing of might";
}
break; break;
case CLASS_PALADIN: case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION) if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
{
return "blessing of might"; return "blessing of might";
}
break; break;
} }
@ -131,32 +197,41 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot) inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
{ {
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return ""; return "";
Player* tp = target->ToPlayer(); Player* targetPlayer = target->ToPlayer();
if (!tp) if (!targetPlayer)
return ""; return "";
if (auto* ai = GET_PLAYERBOT_AI(bot)) if (auto* botAI = GET_PLAYERBOT_AI(bot))
{ {
if (Unit* mt = ai->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get()) if (Unit* mainTank =
botAI->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
{ {
if (mt == target) if (mainTank == target)
return "blessing of sanctuary"; return "blessing of sanctuary";
} }
} }
if (tp->HasTankSpec()) if (targetPlayer->HasTankSpec())
return "blessing of sanctuary"; return "blessing of sanctuary";
return ""; return "";
} }
Value<Unit*>* CastBlessingOnPartyAction::GetTargetValue() Unit* CastBlessingOfMightOnPartyAction::GetTarget()
{ {
if (IsGreaterBlessingMode(bot))
return nullptr;
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell)); return FindBlessingTarget(bot, botAI, [&](Player* player)
{
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
} }
bool CastBlessingOfMightAction::Execute(Event /*event*/) bool CastBlessingOfMightAction::Execute(Event /*event*/)
@ -166,9 +241,6 @@ bool CastBlessingOfMightAction::Execute(Event /*event*/)
return false; return false;
std::string castName = GetActualBlessingOfMight(target); std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target); return botAI->CastSpell(castName, target);
} }
@ -176,20 +248,22 @@ Value<Unit*>* CastBlessingOfMightOnPartyAction::GetTargetValue()
{ {
return context->GetValue<Unit*>( return context->GetValue<Unit*>(
"party member without aura", "party member without aura",
"blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary" "blessing of might,greater blessing of might,blessing of wisdom,"
"greater blessing of wisdom,blessing of sanctuary,"
"greater blessing of sanctuary"
); );
} }
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/) bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
{ {
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
std::string castName = GetActualBlessingOfMight(target); std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target); return botAI->CastSpell(castName, target);
} }
@ -200,45 +274,58 @@ bool CastBlessingOfWisdomAction::Execute(Event /*event*/)
return false; return false;
std::string castName = GetActualBlessingOfWisdom(target); std::string castName = GetActualBlessingOfWisdom(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target); return botAI->CastSpell(castName, target);
} }
Unit* CastBlessingOfWisdomOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player))
return false;
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
}
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue() Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
{ {
return context->GetValue<Unit*>( return context->GetValue<Unit*>(
"party member without aura", "party member without aura",
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary" "blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,"
"blessing of sanctuary,greater blessing of sanctuary"
); );
} }
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/) bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
{ {
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
Player* targetPlayer = target->ToPlayer(); Player* targetPlayer = target->ToPlayer();
if (Group* g = bot->GetGroup()) if (Group* group = bot->GetGroup())
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
return false; return false;
if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) && if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) &&
targetPlayer && IsTankRole(targetPlayer)) targetPlayer && IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName());
return false; return false;
}
std::string castName = GetActualBlessingOfWisdom(target); std::string castName = GetActualBlessingOfWisdom(target);
if (castName.empty()) if (castName.empty())
return false; return false;
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target); return botAI->CastSpell(castName, target);
} }
@ -252,32 +339,31 @@ Value<Unit*>* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue()
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
{ {
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) if (IsGreaterBlessingMode(bot))
return false;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return false; return false;
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
{
// Fallback: GetTarget() can be null if no one needs a buff.
// Keep a valid pointer for the checks/logs that follow.
target = bot; target = bot;
}
Player* targetPlayer = target ? target->ToPlayer() : nullptr; Player* targetPlayer = target ? target->ToPlayer() : nullptr;
// Small helpers to check relevant auras const auto HasKingsAura = [&](Unit* unit) -> bool {
const auto HasKingsAura = [&](Unit* u) -> bool { return botAI->HasAura("blessing of kings", unit) ||
return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u); botAI->HasAura("greater blessing of kings", unit);
}; };
const auto HasSanctAura = [&](Unit* u) -> bool { const auto HasSanctAura = [&](Unit* unit) -> bool {
return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u); return botAI->HasAura("blessing of sanctuary", unit) ||
botAI->HasAura("greater blessing of sanctuary", unit);
}; };
if (Group* g = bot->GetGroup()) if (Group* group = bot->GetGroup())
{ {
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
{ {
LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring");
target = bot; target = bot;
targetPlayer = bot->ToPlayer(); targetPlayer = bot->ToPlayer();
} }
@ -288,9 +374,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
bool selfHasSanct = HasSanctAura(self); bool selfHasSanct = HasSanctAura(self);
bool needSelf = IsTankRole(self) && !selfHasSanct; bool needSelf = IsTankRole(self) && !selfHasSanct;
LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}",
bot->GetName(), IsTankRole(self), selfHasSanct, needSelf);
if (needSelf) if (needSelf)
{ {
target = self; target = self;
@ -298,7 +381,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
} }
} }
// Try to re-target a valid tank in group if needed
bool targetOk = false; bool targetOk = false;
if (targetPlayer) if (targetPlayer)
{ {
@ -308,20 +390,20 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
if (!targetOk) if (!targetOk)
{ {
if (Group* g = bot->GetGroup()) if (Group* group = bot->GetGroup())
{ {
for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next()) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{ {
Player* p = gref->GetSource(); Player* player = ref->GetSource();
if (!p) continue; if (!player) continue;
if (!p->IsInWorld() || !p->IsAlive()) continue; if (!player->IsInWorld() || !player->IsAlive()) continue;
if (!IsTankRole(p)) continue; if (!IsTankRole(player)) continue;
bool hasSanct = HasSanctAura(p); bool hasSanct = HasSanctAura(player);
if (!hasSanct) if (!hasSanct)
{ {
target = p; // prioritize this tank target = player;
targetPlayer = p; targetPlayer = player;
targetOk = true; targetOk = true;
break; break;
} }
@ -329,150 +411,147 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
} }
} }
{ if (GetActualBlessingOfSanctuary(target, bot).empty())
bool hasKings = HasKingsAura(target);
bool hasSanct = HasSanctAura(target);
bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY);
LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}",
target->GetName(), hasKings, hasSanct, knowSanct);
}
std::string castName = GetActualBlessingOfSanctuary(target, bot);
// If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary
if (castName.empty())
{ {
if (targetPlayer) if (targetPlayer)
{ {
if (IsTankRole(targetPlayer)) if (IsTankRole(targetPlayer))
castName = "blessing of sanctuary"; // force single-target return botAI->CastSpell("blessing of sanctuary", target);
else else
return false; return false;
} }
else else
return false; return false;
} }
if (targetPlayer && !IsTankRole(targetPlayer))
{
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
else
{
castName = "blessing of sanctuary";
}
bool ok = botAI->CastSpell(castName, target); return botAI->CastSpell("blessing of sanctuary", target);
LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok); }
return ok;
Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
return IsTankRole(player) &&
!HasBlessingAura(botAI, player,
{ "blessing of sanctuary", "greater blessing of sanctuary" });
});
} }
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue() Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
{ {
return context->GetValue<Unit*>( return context->GetValue<Unit*>(
"party member without aura", "party member without aura",
"blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary" "blessing of kings,greater blessing of kings,"
"blessing of sanctuary,greater blessing of sanctuary"
); );
} }
Unit* CastBlessingOfKingsOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot);
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
const bool isTank = IsTankRole(player);
const bool hasKingsOrSanct = HasBlessingAura(botAI, player,
{ "blessing of kings", "greater blessing of kings",
"blessing of sanctuary", "greater blessing of sanctuary" });
if (hasKingsOrSanct)
return false;
if (hasBwisdom)
return isTank;
if (hasBkings)
{
if (isTank)
return false;
if (onlyPaladinInGroup && player == bot)
return false;
}
return true;
});
}
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/) bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
{ {
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
Group* g = bot->GetGroup(); Group* group = bot->GetGroup();
if (!g) if (!group)
return false; return false;
// Added for patch solo paladin, never buff itself to not remove his sanctuary buff if (botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT) &&
if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot)) IsOnlyPaladinInGroup(bot))
{ {
if (target->GetGUID() == bot->GetGUID()) if (target->GetGUID() == bot->GetGUID())
{
LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName());
return false; return false;
} }
}
// End solo paladin patch
Player* targetPlayer = target->ToPlayer(); Player* targetPlayer = target->ToPlayer();
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
return false; return false;
const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT); const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT); const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
if (hasBmana) if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer)))
{
if (!targetPlayer || !IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName());
return false; return false;
}
}
if (targetPlayer) if (targetPlayer)
{ {
const bool isTank = IsTankRole(targetPlayer); const bool isTank = IsTankRole(targetPlayer);
const bool hasSanctFromMe = const bool hasSanctFromMe =
target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) || target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID()); target->HasAura(ai::paladin::SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
const bool hasSanctAny = const bool hasSanctAny =
botAI->HasAura("blessing of sanctuary", target) || botAI->HasAura("blessing of sanctuary", target) ||
botAI->HasAura("greater blessing of sanctuary", target); botAI->HasAura("greater blessing of sanctuary", target);
if (isTank && hasSanctFromMe) if (isTank && hasSanctFromMe)
{ return false;
LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName());
if (hasBkings && isTank && hasSanctAny)
return false; return false;
} }
if (hasBstats && isTank && hasSanctAny) return botAI->CastSpell("blessing of kings", target);
{
LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName());
return false;
}
}
std::string castName = "blessing of kings";
bool allowGreater = true;
if (hasBmana)
allowGreater = false;
if (allowGreater && hasBstats && targetPlayer)
{
switch (targetPlayer->getClass())
{
case CLASS_WARRIOR:
case CLASS_PALADIN:
case CLASS_DRUID:
case CLASS_DEATH_KNIGHT:
allowGreater = false;
break;
default:
break;
}
}
if (allowGreater)
{
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
return botAI->CastSpell(castName, target);
} }
bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); } bool CastSealSpellAction::isUseful()
{
return AI_VALUE2(bool, "combat", "self target");
}
Value<Unit*>* CastTurnUndeadAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", getName()); } Value<Unit*>* CastTurnUndeadAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", getName());
}
Unit* CastHandOfFreedomOnPartyAction::GetTarget() Unit* CastHandOfFreedomOnPartyAction::GetTarget()
{ {
bool const selfImpaired = botAI->IsMovementImpaired(bot); bool const selfImpaired = botAI->IsMovementImpaired(bot);
bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); bool const hasSelfHand =
selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
if (!bot->GetGroup()) if (!bot->GetGroup())
{ {
@ -499,7 +578,8 @@ bool CastHandOfFreedomOnPartyAction::isUseful()
if (!target) if (!target)
return false; return false;
return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); return CastBuffSpellAction::isUseful() &&
!ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
} }
Unit* CastRighteousDefenseAction::GetTarget() Unit* CastRighteousDefenseAction::GetTarget()

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1462,15 +1462,6 @@ bool HodirBitingColdJumpAction::Execute(Event /*event*/)
// float speed = 7.96f; // float speed = 7.96f;
// UpdateMovementState(); // UpdateMovementState();
// if (!IsMovingAllowed(mapId, x, y, z))
//{
// return false;
// }
// MovementPriority priority;
// if (IsWaitingForLastMove(priority))
//{
// return false;
// }
// MotionMaster& mm = *bot->GetMotionMaster(); // MotionMaster& mm = *bot->GetMotionMaster();
// mm.Clear(); // mm.Clear();

View File

@ -19,6 +19,7 @@
#include "PathGenerator.h" #include "PathGenerator.h"
#include "Player.h" #include "Player.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Playerbots.h"
#include "QuestDef.h" #include "QuestDef.h"
#include "Random.h" #include "Random.h"
#include "SharedDefines.h" #include "SharedDefines.h"
@ -120,17 +121,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
} }
break; break;
} }
case RPG_TRAVEL_FLIGHT: // RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
{ // so the flight action owns both take-off and landing transitions.
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
if (data.inFlight && !bot->IsInFlight())
{
// flight arrival
info.ChangeToIdle();
return true;
}
break;
}
case RPG_REST: case RPG_REST:
{ {
// REST -> IDLE // REST -> IDLE
@ -301,6 +293,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = WorldPosition(); data.pos = WorldPosition();
data.objectiveIdx = 0; data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
} }
} }
if (data.pos == WorldPosition()) if (data.pos == WorldPosition())
@ -329,15 +324,34 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = pos; data.pos = pos;
data.objectiveIdx = objectiveIdx; data.objectiveIdx = objectiveIdx;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
} }
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{ {
// Yield to attack-anything ONLY if a mob needed by this exact
// quest+objective is right next to us. The broad variant (any
// quest in the log) yielded for every nearby mob and derailed
// turn-ins / cross-zone travel through other quests' clusters.
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
return false;
// Note: previously yielded ~10%/tick when any hostile was
// within 25y. That overrode the do-quest multiplier in
// practice (combined with bots getting aggroed on the way,
// which ALSO bypasses the multiplier via combat engine) and
// bots ended up grinding their way to POIs instead of
// travelling. Quest-mob exception above is kept so we don't
// walk past a quest target while gathering. Anything else
// hostile is the multiplier's job to throttle — and bots
// that DO get aggroed switch to combat engine where the
// class strategy handles it.
if (MoveFarTo(data.pos)) if (MoveFarTo(data.pos))
return true; return true;
// Long-range sampler couldn't land a candidate — nudge the // sampler found nothing — nudge so next tick tries a new pos
// bot a short distance so the next tick retries from a
// different position instead of sitting idle.
return MoveRandomNear(10.0f); return MoveRandomNear(10.0f);
} }
// Now we are near the quest objective // Now we are near the quest objective
@ -382,14 +396,111 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = WorldPosition(); data.pos = WorldPosition();
data.objectiveIdx = 0; data.objectiveIdx = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
return true; return true;
} }
// At the POI: keep the bot actively placed but avoid large // at POI: drive toward specific objectives first
// random 20yd hops that look like pacing back and forth. A small if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
// ~8yd wander reads as the bot looking around while grind/loot return true;
// strategies do their work. if (TryLootQuestGO(data.pursuedLootGO))
return MoveRandomNear(8.0f); return true;
if (TryUseQuestGO(data.pursuedUseGO))
return true;
// gather quests: roam for spawns. kill quests: yield to grind.
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest)
{
int32 obj = data.objectiveIdx;
bool isGatherObjective = false;
if (obj < QUEST_OBJECTIVES_COUNT)
{
int32 entry = quest->RequiredNpcOrGo[obj];
if (entry < 0) // GO objective
isGatherObjective = true;
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && quest->RequiredItemId[obj])
isGatherObjective = true;
}
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
isGatherObjective = true;
}
// source-item quest: need to find the target to use it on
if (quest->GetSrcItemId())
isGatherObjective = true;
if (isGatherObjective)
return MoveRandomNear(20.0f);
}
// Kill-quest scout: at POI for 30s+ with no quest mob in sight
// means this cluster is empty. Switch to a different POI candidate
// (>50y away) if one exists; otherwise roam in place.
constexpr uint32 scoutTimeoutMs = 30 * 1000;
if (data.lastReachPOI && GetMSTimeDiffToNow(data.lastReachPOI) >= scoutTimeoutMs &&
!HasNearbyQuestMob(30.0f))
{
std::vector<POIInfo> poiInfo;
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
{
std::vector<size_t> alternatives;
for (size_t i = 0; i < poiInfo.size(); ++i)
{
float dx = poiInfo[i].pos.x - data.pos.GetPositionX();
float dy = poiInfo[i].pos.y - data.pos.GetPositionY();
if (dx * dx + dy * dy > 50.0f * 50.0f)
alternatives.push_back(i);
}
if (!alternatives.empty())
{
size_t pickIdx = alternatives[urand(0, alternatives.size() - 1)];
G3D::Vector2 newPoi = poiInfo[pickIdx].pos;
float dz = std::max(bot->GetMap()->GetHeight(newPoi.x, newPoi.y, MAX_HEIGHT),
bot->GetMap()->GetWaterLevel(newPoi.x, newPoi.y));
if (dz != INVALID_HEIGHT && dz != VMAP_INVALID_HEIGHT_VALUE)
{
data.pos = WorldPosition(bot->GetMapId(), newPoi.x, newPoi.y, dz);
data.objectiveIdx = poiInfo[pickIdx].objectiveIdx;
data.lastReachPOI = 0;
data.pursuedLootGO.Clear();
data.pursuedUseGO.Clear();
data.pursuedUseTarget.Clear();
return true;
}
}
}
return MoveRandomNear(20.0f);
}
// kill quest: walk toward the marker before handing off to grind.
// lastReachPOI trips at ~10y so without this the bot fights on the
// edge and never reaches the dense cluster. Skip if a quest mob is
// in sight (might be the target) or a hostile is mid-pull.
if (bot->GetDistance(data.pos) > 5.0f)
{
if (HasNearbyQuestMob(30.0f))
return false;
GuidVector nearby = AI_VALUE(GuidVector, "possible targets");
bool hostileClose = false;
for (ObjectGuid guid : nearby)
{
Unit* u = botAI->GetUnit(guid);
if (u && u->IsAlive() && bot->GetDistance(u) < 15.0f)
{
hostileClose = true;
break;
}
}
if (!hostileClose)
return MoveFarTo(data.pos);
}
// yield to grind
return false;
} }
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
@ -423,6 +534,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
data.lastReachPOI = 0; data.lastReachPOI = 0;
data.pos = pos; data.pos = pos;
data.objectiveIdx = -1; data.objectiveIdx = -1;
// Drop the spline + lastPath that DoIncompleteQuest committed
// to the now-completed objective. Without this, MoveFarTo on
// the next tick hits the bot->isMoving() / lastPath-reuse
// early-exits at the top of MoveFarTo and rides the stale
// path instead of replanning toward the turn-in POI. (This
// is what `.playerbot bot self` masks by recreating the AI.)
bot->GetMotionMaster()->Clear();
AI_VALUE(LastMovement&, "last movement").clear();
} }
if (data.pos == WorldPosition()) if (data.pos == WorldPosition())
@ -453,7 +573,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
botAI->rpgInfo.ChangeToIdle(); botAI->rpgInfo.ChangeToIdle();
return true; return true;
} }
return false; // waiting for SearchQuestGiverAndAcceptOrReward to pick up the NPC;
// wander instead of false so we don't fall through to grind
return MoveRandomNear(15.0f);
} }
bool NewRpgTravelFlightAction::Execute(Event /*event*/) bool NewRpgTravelFlightAction::Execute(Event /*event*/)
@ -464,6 +586,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
return false; return false;
auto& data = *dataPtr; auto& data = *dataPtr;
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
// so this branch falls through to ChangeToIdle on the next tick.
if (data.inFlight && !bot->IsInFlight())
{
if (bot->GetZoneId() == AREA_TELDRASSIL)
{
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
return MoveFarTo(rutTheranPortalEntrance);
}
info.ChangeToIdle();
return true;
}
if (bot->IsInFlight()) if (bot->IsInFlight())
{ {
data.inFlight = true; data.inFlight = true;
@ -479,19 +617,9 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
info.ChangeToIdle(); info.ChangeToIdle();
return true; return true;
} }
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster);
std::vector<uint32> nodes = data.path; if (!TakeFlight(data.path, flightMaster))
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
{ {
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
info.ChangeToIdle(); info.ChangeToIdle();
return true; return true;
} }

View File

@ -47,11 +47,11 @@ public:
protected: protected:
// static NewRpgStatusTransitionProb transitionMat; // static NewRpgStatusTransitionProb transitionMat;
const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ; const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS;
const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ; const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS;
const int32 statusRestDuration = 30 * IN_MILLISECONDS ; const int32 statusRestDuration = 30 * IN_MILLISECONDS;
const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ; const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS;
const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ; const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS;
}; };
class NewRpgGoGrindAction : public NewRpgBaseAction class NewRpgGoGrindAction : public NewRpgBaseAction

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ protected:
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
/* QUEST RELATED CHECK */ /* QUEST RELATED CHECK */
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f); ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
@ -50,6 +51,29 @@ protected:
bool TurnInQuest(Quest const* quest, ObjectGuid guid); bool TurnInQuest(Quest const* quest, ObjectGuid guid);
bool OrganizeQuestLog(); bool OrganizeQuestLog();
/* QUEST PROGRESSION HELPERS (at POI) */
// Walk to a GO that drops a needed quest item. The loot strategy
// opens and loots it once in range.
bool TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Walk to / use a GO that is itself the objective (rune, lever,
// altar, coffin — RequiredNpcOrGo with a negative entry).
bool TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
// Fire a quest item's OnUse spell at the right target: a spell-focus
// GO (moonwell), a required creature, or the bot itself.
bool TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange = 60.0f);
// True when a quest-relevant mob is within range — used during
// travel so we yield to attack-anything instead of running past.
bool HasNearbyQuestMob(float range = 20.0f);
// Narrower variant: only yields for mobs needed by the SPECIFIC
// quest+objective the bot is currently working on. Without this,
// do-quest yields for any quest in the log, derailing turn-ins
// and cross-zone travel through other quests' mob clusters.
bool HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx);
protected: 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);
@ -58,17 +82,18 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus); bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status); bool CheckRpgStatusAvailable(NewRpgStatus status);
protected: private:
/* FOR MOVE FAR */ void StartTravelPlan(WorldPosition dest);
const float pathFinderDis = 70.0f; bool UpdateTravelPlan();
// Time without real progress toward dest before MoveFarTo
// falls back to teleport recovery. Kept short enough that a // Centralized dispatch helper. Applies underwater fixup, ClipPath
// bot truly oscillating around an unreachable destination // (truncate at first hostile in attack range with LOS, level+5 cap),
// (mmap returning non-progressing partial paths, or NOPATH + // inactive-bot teleport (with self-bot carve-out), masterWalking
// cone fallback wandering) doesn't spin for 5 minutes before // mode, pre-dispatch state cleanup, then dispatches via
// the teleport fires, but long enough that a genuine long // MoveSplinePath and schedules via WaitForReach formula.
// walk that is slowly making progress never triggers it. bool DispatchPathPoints(WorldPosition const& dest,
const uint32 stuckTime = 90 * 1000; Movement::PointsArray& points,
char const* label);
}; };
#endif #endif

View File

@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
botAI->rpgInfo.ChangeToIdle(); botAI->rpgInfo.ChangeToIdle();
return false; return false;
} }
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive()) if (!bot->IsOutdoorPvPActive())
return false; return false;
uint32 zoneId = bot->GetZoneId(); uint32 zoneId = bot->GetZoneId();
@ -113,9 +113,6 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius) bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
{ {
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false;
// Randomly pause at the current spot before picking a new patrol point // Randomly pause at the current spot before picking a new patrol point
if (urand(0, 2) == 0) if (urand(0, 2) == 0)
return ForceToWait(urand(3000, 6000)); return ForceToWait(urand(3000, 6000));

View File

@ -6,31 +6,31 @@
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos) void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
{ {
startT = getMSTime(); Reset();
data = GoGrind{pos}; data = GoGrind{pos};
} }
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos) void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
{ {
startT = getMSTime(); Reset();
data = GoCamp{pos}; data = GoCamp{pos};
} }
void NewRpgInfo::ChangeToWanderNpc() void NewRpgInfo::ChangeToWanderNpc()
{ {
startT = getMSTime(); Reset();
data = WanderNpc{}; data = WanderNpc{};
} }
void NewRpgInfo::ChangeToWanderRandom() void NewRpgInfo::ChangeToWanderRandom()
{ {
startT = getMSTime(); Reset();
data = WanderRandom{}; data = WanderRandom{};
} }
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
{ {
startT = getMSTime(); Reset();
DoQuest do_quest; DoQuest do_quest;
do_quest.questId = questId; do_quest.questId = questId;
do_quest.quest = quest; do_quest.quest = quest;
@ -39,7 +39,7 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path) void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
{ {
startT = getMSTime(); Reset();
TravelFlight flight; TravelFlight flight;
flight.flightMasterEntry = flightMasterEntry; flight.flightMasterEntry = flightMasterEntry;
flight.flightMasterPos = flightMasterPos; flight.flightMasterPos = flightMasterPos;
@ -58,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
void NewRpgInfo::ChangeToRest() void NewRpgInfo::ChangeToRest()
{ {
startT = getMSTime(); Reset();
data = Rest{}; data = Rest{};
} }
void NewRpgInfo::ChangeToIdle() void NewRpgInfo::ChangeToIdle()
{ {
startT = getMSTime(); Reset();
data = Idle{}; data = Idle{};
} }
@ -77,14 +77,7 @@ void NewRpgInfo::Reset()
{ {
data = Idle{}; data = Idle{};
startT = getMSTime(); startT = getMSTime();
} ClearTravel();
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
{
nearestMoveFarDis = FLT_MAX;
stuckTs = 0;
stuckAttempts = 0;
moveFarPos = pos;
} }
NewRpgStatus NewRpgInfo::GetStatus() NewRpgStatus NewRpgInfo::GetStatus()

View File

@ -1,6 +1,8 @@
#ifndef _PLAYERBOT_NEWRPGINFO_H #ifndef _PLAYERBOT_NEWRPGINFO_H
#define _PLAYERBOT_NEWRPGINFO_H #define _PLAYERBOT_NEWRPGINFO_H
#include <deque>
#include "Define.h" #include "Define.h"
#include "ObjectGuid.h" #include "ObjectGuid.h"
#include "ObjectMgr.h" #include "ObjectMgr.h"
@ -8,6 +10,7 @@
#include "Strategy.h" #include "Strategy.h"
#include "Timer.h" #include "Timer.h"
#include "TravelMgr.h" #include "TravelMgr.h"
#include "TravelNode.h"
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>; using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
@ -45,6 +48,11 @@ struct NewRpgInfo
int32 objectiveIdx{0}; int32 objectiveIdx{0};
WorldPosition pos{}; WorldPosition pos{};
uint32 lastReachPOI{0}; uint32 lastReachPOI{0};
// committed target per objective type. stops zig-zagging in
// dense spawn clusters when "nearest" would flip each tick.
ObjectGuid pursuedLootGO{}; // GOs we loot (lilies, eggs)
ObjectGuid pursuedUseGO{}; // GOs we click or focus on
ObjectGuid pursuedUseTarget{}; // creature we apply an item to
}; };
// RPG_TRAVEL_FLIGHT // RPG_TRAVEL_FLIGHT
struct TravelFlight struct TravelFlight
@ -70,12 +78,10 @@ struct NewRpgInfo
uint32 startT{0}; // start timestamp of the current status uint32 startT{0}; // start timestamp of the current status
// MOVE_FAR // Travel Node System
float nearestMoveFarDis{FLT_MAX}; TravelPlan travelPlan;
uint32 stuckTs{0}; bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
uint32 stuckAttempts{0}; void ClearTravel() { travelPlan.Reset(); }
WorldPosition moveFarPos;
// END MOVE_FAR
using RpgData = std::variant< using RpgData = std::variant<
Idle, Idle,
@ -103,7 +109,6 @@ struct NewRpgInfo
void ChangeToIdle(); void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status); bool CanChangeTo(NewRpgStatus status);
void Reset(); void Reset();
void SetMoveFarTo(WorldPosition pos);
std::string ToString(); std::string ToString();
}; };

View File

@ -5,11 +5,66 @@
#include "NewRpgStrategy.h" #include "NewRpgStrategy.h"
#include "Action.h"
#include "NewRpgInfo.h"
#include "Player.h"
#include "PlayerbotAI.h"
static bool IsGatherObjectiveForDoQuest(NewRpgInfo::DoQuest const* data)
{
if (!data || !data->quest)
return false;
Quest const* q = data->quest;
int32 obj = data->objectiveIdx;
if (obj < QUEST_OBJECTIVES_COUNT)
{
int32 entry = q->RequiredNpcOrGo[obj];
if (entry < 0) // GO objective
return true;
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && q->RequiredItemId[obj])
return true;
}
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
return true;
}
// source-item quest: need to find the right target to use it on
if (q->GetSrcItemId())
return true;
return false;
}
float NewRpgDoQuestMultiplier::GetValue(Action* action)
{
if (!action || action->getName() != "attack anything")
return 1.0f;
NewRpgInfo& info = botAI->rpgInfo;
if (info.GetStatus() != RPG_DO_QUEST)
return 1.0f;
auto* data = std::get_if<NewRpgInfo::DoQuest>(&info.data);
if (!data)
return 1.0f;
// heading back to turn in, don't get sidetracked
if (data->questId && bot->GetQuestStatus(data->questId) == QUEST_STATUS_COMPLETE)
return 0.15f;
// at POI: gather stays low so mobs don't pull us off the cluster;
// kill runs full so attack-anything drives behavior
if (data->lastReachPOI)
return IsGatherObjectiveForDoQuest(data) ? 0.30f : 1.0f;
// traveling
return 0.20f;
}
NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
std::vector<NextAction> NewRpgStrategy::getDefaultActions() std::vector<NextAction> NewRpgStrategy::getDefaultActions()
{ {
// the releavance should be greater than grind // must outrank grind
return { return {
NextAction("new rpg status update", 11.0f) NextAction("new rpg status update", 11.0f)
}; };
@ -53,7 +108,8 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode( new TriggerNode(
"do quest status", "do quest status",
{ {
NextAction("new rpg do quest", 3.0f) // 4.5: above attack-anything (4.0), below loot (5.0+)
NextAction("new rpg do quest", 4.5f)
} }
) )
); );
@ -75,6 +131,7 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
); );
} }
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>&) void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
{ {
multipliers.push_back(new NewRpgDoQuestMultiplier(botAI));
} }

View File

@ -12,6 +12,13 @@
class PlayerbotAI; class PlayerbotAI;
class NewRpgDoQuestMultiplier : public Multiplier
{
public:
NewRpgDoQuestMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "new rpg do quest") {}
float GetValue(Action* action) override;
};
class NewRpgStrategy : public Strategy class NewRpgStrategy : public Strategy
{ {
public: public:

View File

@ -500,21 +500,21 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const
switch (player->getClass()) switch (player->getClass())
{ {
case CLASS_PRIEST: case CLASS_PRIEST:
nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); nonCombatEngine->addStrategiesNoInit("dps assist", "cure", "rshadow", nullptr);
break; break;
case CLASS_PALADIN: case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION) if (tab == PALADIN_TAB_PROTECTION)
{ {
nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr); nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr);
if (player->GetLevel() >= 20) if (player->GetLevel() >= 20)
nonCombatEngine->addStrategy("bhealth", false); nonCombatEngine->addStrategy("bsanc", false);
else else
nonCombatEngine->addStrategy("bdps", false); nonCombatEngine->addStrategy("bmight", false);
} }
else if (tab == PALADIN_TAB_HOLY) else if (tab == PALADIN_TAB_HOLY)
nonCombatEngine->addStrategiesNoInit("dps assist", "bmana", "bcast", nullptr); nonCombatEngine->addStrategiesNoInit("dps assist", "bwisdom", "bcast", nullptr);
else else
nonCombatEngine->addStrategiesNoInit("dps assist", "bdps", "baoe", nullptr); nonCombatEngine->addStrategiesNoInit("dps assist", "bmight", "baoe", nullptr);
nonCombatEngine->addStrategiesNoInit("cure", nullptr); nonCombatEngine->addStrategiesNoInit("cure", nullptr);
break; break;

View File

@ -767,6 +767,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
} }
} }
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
{
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
return;
bot->GetMotionMaster()->Clear();
if (resetAI)
Reset(true);
else
InterruptSpell();
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
bot->SendMovementFlagUpdate();
}
void PlayerbotAI::HandleTeleportAck() void PlayerbotAI::HandleTeleportAck()
{ {
if (!bot || !bot->GetSession()) if (!bot || !bot->GetSession())
@ -809,7 +824,7 @@ void PlayerbotAI::HandleTeleportAck()
bot->StopMoving(); bot->StopMoving();
} }
// simulate far teleport latency (cmangos-style) // simulate far teleport latency
SetNextCheckDelay(urand(2000, 5000)); SetNextCheckDelay(urand(2000, 5000));
return; return;
} }
@ -5971,29 +5986,6 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot)
LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str()); LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str());
} }
uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname)
{
uint32 bcount = 0;
if (Group* group = bot->GetGroup())
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld())
continue;
if (!member->IsInSameRaidWith(player))
continue;
if (HasAura(spellname, member, true))
bcount++;
}
}
return bcount;
}
int32 PlayerbotAI::GetNearGroupMemberCount(float dis) int32 PlayerbotAI::GetNearGroupMemberCount(float dis)
{ {
int count = 1; // yourself int count = 1; // yourself

View File

@ -396,6 +396,7 @@ public:
void HandleMasterIncomingPacket(WorldPacket const& packet); void HandleMasterIncomingPacket(WorldPacket const& packet);
void HandleMasterOutgoingPacket(WorldPacket const& packet); void HandleMasterOutgoingPacket(WorldPacket const& packet);
void HandleTeleportAck(); void HandleTeleportAck();
void TeleportTo(WorldLocation loc, bool resetAI = false);
void ChangeEngine(BotState type); void ChangeEngine(BotState type);
void ChangeEngineOnCombat(); void ChangeEngineOnCombat();
void ChangeEngineOnNonCombat(); void ChangeEngineOnNonCombat();
@ -493,7 +494,6 @@ public:
void ImbueItem(Item* item, Unit* target); void ImbueItem(Item* item, Unit* target);
void ImbueItem(Item* item); void ImbueItem(Item* item);
void EnchantItemT(uint32 spellid, uint8 slot); void EnchantItemT(uint32 spellid, uint8 slot);
uint32 GetBuffedCount(Player* player, std::string const spellname);
int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance); int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance);
virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr); virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr);

View File

@ -1697,14 +1697,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
break; break;
} }
bot->GetMotionMaster()->Clear(); botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true);
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI)
botAI->Reset(true);
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
bot->SendMovementFlagUpdate();
if (pmo) if (pmo)
pmo->finish(); pmo->finish();

View File

@ -45,7 +45,8 @@ void LootTargetList::shrink(time_t fromTime)
} }
} }
LootObject::LootObject(Player* bot, ObjectGuid guid) : guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0) LootObject::LootObject(Player* bot, ObjectGuid guid)
: guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0), isNeededQuestItem(false)
{ {
Refresh(bot, guid); Refresh(bot, guid);
} }
@ -55,6 +56,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
skillId = SKILL_NONE; skillId = SKILL_NONE;
reqSkillValue = 0; reqSkillValue = 0;
reqItem = 0; reqItem = 0;
isNeededQuestItem = false;
guid.Clear(); guid.Clear();
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
@ -101,6 +103,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (IsNeededForQuest(bot, itemId)) if (IsNeededForQuest(bot, itemId))
{ {
this->guid = lootGUID; this->guid = lootGUID;
this->isNeededQuestItem = true;
return; return;
} }
@ -135,10 +138,21 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (!proto) if (!proto)
continue; continue;
// Moonpetal Lily, Hyacinth Mushroom etc. expose quest
// drops here (not in gameobject_questitem). Flag it so
// the INTERACT_COND gate lets the bot through.
if (IsNeededForQuest(bot, itemId))
{
this->guid = lootGUID;
this->isNeededQuestItem = true;
return;
}
if (proto->Class != ITEM_CLASS_QUEST) if (proto->Class != ITEM_CLASS_QUEST)
{ {
onlyHasQuestItems = false; onlyHasQuestItems = false;
break; // keep scanning — a later item may be needed
continue;
} }
// If this item references another loot table, process it // If this item references another loot table, process it
@ -157,11 +171,15 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
if (!refProto) if (!refProto)
continue; continue;
if (refProto->Class != ITEM_CLASS_QUEST) if (IsNeededForQuest(bot, refItemId))
{ {
onlyHasQuestItems = false; this->guid = lootGUID;
break; this->isNeededQuestItem = true;
return;
} }
if (refProto->Class != ITEM_CLASS_QUEST)
onlyHasQuestItems = false;
} }
} }
} }
@ -270,6 +288,7 @@ LootObject::LootObject(LootObject const& other)
skillId = other.skillId; skillId = other.skillId;
reqSkillValue = other.reqSkillValue; reqSkillValue = other.reqSkillValue;
reqItem = other.reqItem; reqItem = other.reqItem;
isNeededQuestItem = other.isNeededQuestItem;
} }
bool LootObject::IsLootPossible(Player* bot) bool LootObject::IsLootPossible(Player* bot)
@ -299,10 +318,13 @@ bool LootObject::IsLootPossible(Player* bot)
return false; return false;
} }
// Prevent bot from running to chests that are unlootable (e.g. Gunship Armory before completing the event) or on // Block event-gated chests (Gunship Armory pre-event) and unspawned
// respawn time // GOs. INTERACT_COND alone is allowed when the GO holds a quest
// item we need — ConditionMgr already gates on quest state.
GameObject* go = botAI->GetGameObject(guid); GameObject* go = botAI->GetGameObject(guid);
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND | GO_FLAG_NOT_SELECTABLE) || !go->isSpawned())) if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
return false;
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !isNeededQuestItem)
return false; return false;
if (skillId == SKILL_NONE) if (skillId == SKILL_NONE)
@ -340,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
bool LootObjectStack::Add(ObjectGuid guid) bool LootObjectStack::Add(ObjectGuid guid)
{ {
// expire old completed entries so a despawn/respawn with a reused
// guid can still be looted later
completedLoot.shrink(time(nullptr) - 300);
if (completedLoot.find(guid) != completedLoot.end())
return false;
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT) if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
{ {
availableLoot.shrink(time(nullptr) - 30); availableLoot.shrink(time(nullptr) - 30);
@ -363,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
availableLoot.erase(i); availableLoot.erase(i);
} }
void LootObjectStack::Clear() { availableLoot.clear(); } void LootObjectStack::MarkCompleted(ObjectGuid guid)
{
Remove(guid);
completedLoot.insert(guid);
}
void LootObjectStack::Clear()
{
availableLoot.clear();
completedLoot.clear();
}
bool LootObjectStack::CanLoot(float maxDistance) bool LootObjectStack::CanLoot(float maxDistance)
{ {

View File

@ -26,7 +26,7 @@ public:
class LootObject class LootObject
{ {
public: public:
LootObject() : skillId(0), reqSkillValue(0), reqItem(0) {} LootObject() : skillId(0), reqSkillValue(0), reqItem(0), isNeededQuestItem(false) {}
LootObject(Player* bot, ObjectGuid guid); LootObject(Player* bot, ObjectGuid guid);
LootObject(LootObject const& other); LootObject(LootObject const& other);
LootObject& operator=(LootObject const& other) = default; LootObject& operator=(LootObject const& other) = default;
@ -40,6 +40,9 @@ public:
uint32 skillId; uint32 skillId;
uint32 reqSkillValue; uint32 reqSkillValue;
uint32 reqItem; uint32 reqItem;
// GO holds a quest item we still need; lets us bypass the
// INTERACT_COND blanket reject in the loot path
bool isNeededQuestItem;
private: private:
static bool IsNeededForQuest(Player* bot, uint32 itemId); static bool IsNeededForQuest(Player* bot, uint32 itemId);
@ -73,6 +76,7 @@ public:
bool Add(ObjectGuid guid); bool Add(ObjectGuid guid);
void Remove(ObjectGuid guid); void Remove(ObjectGuid guid);
void MarkCompleted(ObjectGuid guid);
void Clear(); void Clear();
bool CanLoot(float maxDistance); bool CanLoot(float maxDistance);
LootObject GetLoot(float maxDistance = 0); LootObject GetLoot(float maxDistance = 0);
@ -82,6 +86,9 @@ private:
Player* bot; Player* bot;
LootTargetList availableLoot; LootTargetList availableLoot;
// Guids we already opened loot on; blocks "add all loot" from
// re-adding the same corpse before it despawns.
LootTargetList completedLoot;
}; };
#endif #endif

View File

@ -17,6 +17,7 @@
#include "ChatHelper.h" #include "ChatHelper.h"
#include "MapCollisionData.h" #include "MapCollisionData.h"
#include "MapMgr.h" #include "MapMgr.h"
#include "ModelIgnoreFlags.h"
#include "PathGenerator.h" #include "PathGenerator.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RaceMgr.h" #include "RaceMgr.h"
@ -681,93 +682,6 @@ std::vector<WorldPosition> WorldPosition::frommGridCoord(mGridCoord GridCoord)
return retVec; return retVec;
} }
// TODO: Cleanup — make this actually work.
void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y)
{
std::string const fileName = "load_map_grid.csv";
/*
if (isOverworld() && false || false)
{
if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y))
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
else
{
// This needs to be disabled or maps will not load.
// Needs more testing to check for impact on movement.
if (false)
if (!TravelMgr::instance().isBadVmap(mapId, x, y))
{
// load VMAPs for current map/grid...
const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId);
//const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal.
int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap(
(sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y);
switch (vmapLoadResult)
{
case VMAP::VMAP_LOAD_RESULT_OK:
// LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_ERROR:
// LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{},
// y:{})", mapName, mapId, x, y, x, y);
TravelMgr::instance().addBadVmap(mapId, x, y);
break;
case VMAP::VMAP_LOAD_RESULT_IGNORED:
TravelMgr::instance().addBadVmap(mapId, x, y);
// LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
// mapName, mapId, x, y, x, y);
break;
}
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
*/
if (!TravelMgr::instance().isBadMmap(mapId, x, y))
{
// load navmesh
Map* map = getMap();
if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR)
TravelMgr::instance().addBadMmap(mapId, x, y);
if (sPlayerbotAIConfig.hasLog(fileName))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr();
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
<< ",";
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
sPlayerbotAIConfig.log(fileName, out.str().c_str());
}
}
}
void WorldPosition::loadMapAndVMaps(WorldPosition secondPos)
{
for (auto& grid : getmGridCoords(secondPos))
{
loadMapAndVMap(GetMapId(), grid.first, grid.second);
}
}
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path) std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
{ {
std::vector<WorldPosition> retVec; std::vector<WorldPosition> retVec;
@ -780,39 +694,107 @@ std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vecto
// A single pathfinding attempt from one position to another. Returns pathfinding status and path. // A single pathfinding attempt from one position to another. Returns pathfinding status and path.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot) std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
{ {
if (!bot) Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
if (!pathUnit)
{
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
if (!map)
return {}; return {};
// Load mmaps and vmaps between the two points. tempCreature = new Creature();
loadMapAndVMaps(startPos); if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
startPos.GetPositionX(), startPos.GetPositionY(),
startPos.GetPositionZ(), 0))
{
delete tempCreature;
return {};
}
pathUnit = tempCreature;
PathGenerator path(bot); map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY()));
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ()); map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
// Source is a temp Creature, so CreateFilter's bot block doesn't
// fire — apply the same bot cost biases here so generated paths
// match what bots prefer at runtime (STEEP/water are reachable
// but not preferred).
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
path.SetNavTerrainCost(NAV_WATER, 10.0f);
auto result = getPathStepFrom(startPos, path);
if (tempCreature)
delete tempCreature;
return result;
}
// Pathfinder-reuse overload — caller owns the PathGenerator and any
// per-call configuration. Used by getPathFromPath to thread one
// PathGenerator through the whole 40-step chain instead of
// constructing a new one per step.
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path)
{
// Explicit-start overload. Without this, the chain begins from the
// unit's current position every step and never advances.
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
GetPositionX(), GetPositionY(), GetPositionZ(), false);
Movement::PointsArray points = path.GetPath(); Movement::PointsArray points = path.GetPath();
PathType type = path.GetPathType(); PathType type = path.GetPathType();
if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv")) // PathType is a bitmask. AC's PathGenerator returns
{ // NORMAL | NOT_USING_PATH when start/end poly is INVALID_POLYREF
std::ostringstream out; // (BuildShortcut produces a 2-point straight line through whatever's
out << std::fixed << std::setprecision(1); // in the way). Reject those to avoid silently dispatching a
printWKT({startPos, *this}, out); // geometry-ignoring shortcut.
sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str()); if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
} (type & PATHFIND_NOT_USING_PATH))
if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL))
{
std::ostringstream out;
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
out << std::fixed << std::setprecision(1) << type << ",";
printWKT(fromPointsArray(points), out, 1);
sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str());
}
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
return fromPointsArray(points);
return {}; return {};
std::vector<WorldPosition> retvec = fromPointsArray(points);
// Underwater path-extension. When PATHFIND_INCOMPLETE ends within
// 50y of dest and both endpoints are underwater with LOS, extend
// by one 5y step (or straight to dest if <5y). Lets bots traverse
// navmesh-poor water volumes.
if (type & PATHFIND_INCOMPLETE)
{
WorldPosition end = *this;
WorldPosition lastPoint = retvec.back();
float dist = lastPoint.distance(&end);
if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater())
{
Map* m = end.getMap();
bool inLos = m && m->isInLineOfSight(
lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f,
end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f,
PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
if (inLos)
{
if (dist < 5.0f)
retvec.push_back(end);
else
{
float dx = end.GetPositionX() - lastPoint.GetPositionX();
float dy = end.GetPositionY() - lastPoint.GetPositionY();
float dz = end.GetPositionZ() - lastPoint.GetPositionZ();
float scale = 5.0f / dist;
retvec.emplace_back(end.GetMapId(),
lastPoint.GetPositionX() + dx * scale,
lastPoint.GetPositionY() + dy * scale,
lastPoint.GetPositionZ() + dz * scale);
}
}
}
}
return retvec;
} }
bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance) bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
@ -848,27 +830,63 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
std::vector<WorldPosition> subPath, fullPath = startPath; std::vector<WorldPosition> subPath, fullPath = startPath;
// Construct ONE PathGenerator and thread it through every step
// to avoid the per-step alloc cost. AC's BuildPolyPath has a
// subpath-prefix optimization that can bend chained probes, so
// call Clear() before each step to reset the poly cache.
Unit* pathUnit = bot;
Creature* tempCreature = nullptr;
if (!pathUnit)
{
Map* map = sMapMgr->FindBaseMap(GetMapId());
if (!map)
return fullPath;
tempCreature = new Creature();
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
PHASEMASK_NORMAL, 1 /*entry*/, 0,
currentPos.GetPositionX(), currentPos.GetPositionY(),
currentPos.GetPositionZ(), 0))
{
delete tempCreature;
return fullPath;
}
pathUnit = tempCreature;
map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
}
PathGenerator path(pathUnit);
// Same reason as getPathStepFrom: temp-Creature source doesn't trip
// CreateFilter's bot block, so apply the bot cost biases manually.
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
path.SetNavTerrainCost(NAV_WATER, 10.0f);
// Limit the pathfinding attempts // Limit the pathfinding attempts
for (uint32 i = 0; i < maxAttempt; i++) for (uint32 i = 0; i < maxAttempt; i++)
{ {
// Try to pathfind to this position. // Reset cached poly state from the previous step so each call
subPath = getPathStepFrom(currentPos, bot); // is a fresh A* (otherwise the prefix-recycling at
// PathGenerator.cpp BuildPolyPath snaps the start to the
// cached corridor, bending the chain).
path.Clear();
subPath = getPathStepFrom(currentPos, path);
// If we could not find a path return what we have now.
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance) if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
break; break;
// Append the path excluding the start (this should be the same as the end of the startPath)
fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end()); fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
// Are we there yet?
if (isPathTo(subPath)) if (isPathTo(subPath))
break; break;
// Continue pathfinding.
currentPos = subPath.back(); currentPos = subPath.back();
} }
if (tempCreature)
delete tempCreature;
return fullPath; return fullPath;
} }
@ -1073,6 +1091,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData)
loadedFromDB = true; loadedFromDB = true;
} }
TravelDestination::~TravelDestination()
{
for (WorldPosition* point : points)
delete point;
points.clear();
}
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull) std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
{ {
if (ignoreFull) if (ignoreFull)
@ -2379,9 +2405,7 @@ void TravelMgr::LoadQuestTravelTable()
sPlayerbotAIConfig.openLog("unload_grid.csv", "w"); sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
sPlayerbotAIConfig.openLog("unload_obj.csv", "w"); sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
TravelNodeMap::instance().loadNodeStore(); // Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
TravelNodeMap::instance().generateAll();
/* /*
bool fullNavPointReload = false; bool fullNavPointReload = false;
@ -2772,7 +2796,7 @@ void TravelMgr::LoadQuestTravelTable()
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode)) //if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
// continue; // continue;
startNode->buildPath(endNode, nullptr, false); startNode->BuildPath(endNode, nullptr, false);
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete()) //if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
//startNode->removeLinkTo(endNode); //startNode->removeLinkTo(endNode);
@ -2896,7 +2920,7 @@ void TravelMgr::LoadQuestTravelTable()
TravelNodePath nodePath = *path.second; TravelNodePath nodePath = *path.second;
std::vector<WorldPosition> pPath = nodePath.getPath(); std::vector<WorldPosition> pPath = nodePath.GetPath();
std::reverse(pPath.begin(), pPath.end()); std::reverse(pPath.begin(), pPath.end());
nodePath.setPath(pPath); nodePath.setPath(pPath);
@ -4359,8 +4383,7 @@ void TravelMgr::Init()
PrepareZone2LevelBracket(); PrepareZone2LevelBracket();
PrepareDestinationCache(); PrepareDestinationCache();
} }
sTravelNodeMap.InitTaxiGraph(); sTravelNodeMap.Init();
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
} }
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
@ -4407,7 +4430,7 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
std::vector<std::vector<uint32>> validDestinations; std::vector<std::vector<uint32>> validDestinations;
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot); FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f) if (!nearestFlightMaster)
return validDestinations; return validDestinations;
uint32 fromNode = nearestFlightMaster->taxiNodeId; uint32 fromNode = nearestFlightMaster->taxiNodeId;
@ -4426,9 +4449,9 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId())) if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0; botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
//Simplify destination delection. Its either target cities (Based on config value) or target world.
std::vector<uint32> candidateZones; std::vector<uint32> candidateZones;
if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) if (botLevel >= 10 && !botInCapital &&
urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
{ {
TeamId botTeam = bot->GetTeamId(); TeamId botTeam = bot->GetTeamId();
for (Capital const& capital : capitals) for (Capital const& capital : capitals)
@ -4555,6 +4578,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations; return fallbackLocations;
} }
bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer)
{
uint16 botMapId = bot->GetMapId();
auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache;
auto mapIt = cache.find(botMapId);
if (mapIt == cache.end() || mapIt->second.empty())
return false;
// Collect all areas on this map that have auctioneers
std::vector<uint32> areaIds;
areaIds.reserve(mapIt->second.size());
for (auto const& [areaId, npcs] : mapIt->second)
{
if (!npcs.empty())
areaIds.push_back(areaId);
}
if (areaIds.empty())
return false;
// Pick a random area, then a random auctioneer in that area
uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)];
auto const& auctioneers = mapIt->second.at(selectedArea);
outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)];
return true;
}
void TravelMgr::PrepareZone2LevelBracket() void TravelMgr::PrepareZone2LevelBracket()
{ {
// Classic WoW - starter zones // Classic WoW - starter zones
@ -4641,6 +4692,7 @@ void TravelMgr::PrepareDestinationCache()
uint32 flightMastersCount = 0; uint32 flightMastersCount = 0;
uint32 innkeepersCount = 0; uint32 innkeepersCount = 0;
uint32 bankerCount = 0; uint32 bankerCount = 0;
uint32 auctioneerCount = 0;
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
// Temporary map to group creatures by entry and area // Temporary map to group creatures by entry and area
@ -4687,18 +4739,18 @@ void TravelMgr::PrepareDestinationCache()
(creatureTemplate->unit_flags & 4096) == 0 && (creatureTemplate->unit_flags & 4096) == 0 &&
creatureTemplate->rank == 0) creatureTemplate->rank == 0)
{ {
int32 roundX = static_cast<int32>(std::lround(x / 50.0f)); uint32 roundX = static_cast<uint32>(std::round(x / 50.0f));
int32 roundY = static_cast<int32>(std::lround(y / 50.0f)); uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
int32 roundZ = static_cast<int32>(std::lround(z / 50.0f)); uint32 roundZ = static_cast<uint32>(std::round(z / 50.0f));
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
} }
// FLIGHT MASTERS // FLIGHT MASTERS
// Entry 29480 is Grimwing (Storm Peaks) // Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one. // isn't a real usable flight master; skip it.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) creatureTemplate->Entry != 29480)
{ {
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
bool forHorde = !(factionEntry->hostileMask & 4); bool forHorde = !(factionEntry->hostileMask & 4);
@ -4783,7 +4835,7 @@ void TravelMgr::PrepareDestinationCache()
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
creatureTemplate->Entry != 29282) creatureTemplate->Entry != 29282)
{ {
BankerLocation bLoc; NpcLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = templateEntry; bLoc.entry = templateEntry;
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
@ -4806,6 +4858,31 @@ void TravelMgr::PrepareDestinationCache()
} }
bankerCount++; bankerCount++;
} }
// === AUCTIONEERS ===
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
if (!factionEntry)
continue;
bool forHorde = !(factionEntry->hostileMask & 4);
bool forAlliance = !(factionEntry->hostileMask & 2);
if (!forHorde && !forAlliance)
continue;
NpcLocation aLoc;
aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI);
aLoc.entry = templateEntry;
if (forHorde)
hordeAuctioneerCache[mapId][areaId].push_back(aLoc);
if (forAlliance)
allianceAuctioneerCache[mapId][areaId].push_back(aLoc);
auctioneerCount++;
}
} }
// Process temporary caches // Process temporary caches
@ -4815,16 +4892,29 @@ void TravelMgr::PrepareDestinationCache()
{ {
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
float totalX = 0.0f;
float totalY = 0.0f;
float totalZ = 0.0f;
for (CreatureData const& creatureData : creatureDataList)
{
totalX += creatureData.posX;
totalY += creatureData.posY;
totalZ += creatureData.posZ;
}
float avgX = totalX / creatureDataList.size();
float avgY = totalY / creatureDataList.size();
float avgZ = totalZ / creatureDataList.size();
uint32 mapId = std::get<0>(gridTuple);
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{ {
if (l < 1 || l > maxLevel) if (l < 1 || l > maxLevel)
continue; continue;
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple), locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f));
static_cast<float>(std::get<1>(gridTuple)) * 50.0f,
static_cast<float>(std::get<2>(gridTuple)) * 50.0f,
static_cast<float>(std::get<3>(gridTuple)) * 50.0f));
} }
} }
} }
@ -4870,5 +4960,5 @@ void TravelMgr::PrepareDestinationCache()
break; break;
} }
} }
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount);
} }

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 <cmath>
#include <map> #include <map>
#include <random> #include <random>
@ -18,6 +19,7 @@
class Creature; class Creature;
class GuidPosition; class GuidPosition;
class PathGenerator;
class ObjectGuid; class ObjectGuid;
class Quest; class Quest;
class Player; class Player;
@ -268,12 +270,6 @@ public:
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos); std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord); std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
void loadMapAndVMaps(WorldPosition secondPos);
// Display functions // Display functions
WorldPosition getDisplayLocation(); WorldPosition getDisplayLocation();
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; } float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
@ -288,6 +284,7 @@ public:
// Pathfinding // Pathfinding
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot); std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder);
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40); std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot) std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)
@ -297,10 +294,26 @@ public:
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); } std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
bool isPathTo(std::vector<WorldPosition> path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance) // The path "reaches" this position when its last point is on
// the same map, within maxDistance horizontally, and within
// maxZDistance vertically. 3D Euclidean distance would falsely
// accept paths that end the right horizontal distance from us
// but on a roof/floor below. maxDistance == 0 falls back to
// targetPosRecalcDistance (0.1y).
bool isPathTo(std::vector<WorldPosition> const& path, float const maxDistance = 0.0f,
float const maxZDistance = 2.0f) const
{ {
return !path.empty() && distance(path.back()) < maxDistance; if (path.empty())
}; return false;
WorldPosition const& back = path.back();
if (back.GetMapId() != GetMapId())
return false;
float const realMax = maxDistance > 0.0f ? maxDistance
: sPlayerbotAIConfig.targetPosRecalcDistance;
if (GetExactDist2dSq(&back) >= realMax * realMax)
return false;
return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance;
}
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance); bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); } bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
@ -507,9 +520,15 @@ public:
radiusMin = radiusMin1; radiusMin = radiusMin1;
radiusMax = radiusMax1; radiusMax = radiusMax1;
} }
virtual ~TravelDestination() = default; virtual ~TravelDestination();
void addPoint(WorldPosition* pos) { points.push_back(pos); } void addPoint(WorldPosition* pos)
{
if (!pos)
return;
points.push_back(new WorldPosition(*pos));
}
void setExpireDelay(uint32 delay) { expireDelay = delay; } void setExpireDelay(uint32 delay) { expireDelay = delay; }
@ -673,7 +692,7 @@ public:
bool isActive(Player* bot) override; bool isActive(Player* bot) override;
virtual CreatureTemplate const* GetCreatureTemplate(); virtual CreatureTemplate const* GetCreatureTemplate();
std::string const getName() override { return "RpgTravelDestination"; } std::string const getName() override { return "RpgTravelDestination"; }
int32 getEntry() override { return 0; } int32 getEntry() override { return entry; }
std::string const getTitle() override; std::string const getTitle() override;
protected: protected:
@ -985,18 +1004,14 @@ private:
bool InsideBracket(uint32 val) const { return val >= low && val <= high; } bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
}; };
struct BankerLocation
{
WorldLocation loc;
uint32 entry;
};
// Navigation caches // Navigation caches
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache; std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache; std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache; std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation; std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache; std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate; std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,12 @@
#include <shared_mutex> #include <shared_mutex>
#include "G3D/Vector3.h"
#include "TravelMgr.h" #include "TravelMgr.h"
// THEORY // THEORY
// //
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase. // Pathfinding uses the detour recast navmesh engine for mob, npc, and bot movement.
// This system is used for mob and npc pathfinding and in this codebase also for bots.
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y. // Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a // This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
// straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't // straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't
@ -24,56 +24,90 @@
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E> // <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
// //
// Bot at <S> wants to move to <E> // Bot at <S> wants to move to <E>
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not // [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel. // from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
// //
// Termonology: // Terminology:
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach // Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to // other nodes. Stored in DB table `playerbots_travelnode`.
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one // Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of // A link is one-directional. Stored in `playerbots_travelnode_link`.
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search // Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
// based on links. // A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
// a standard A* search based on links.
// //
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future // Edge types (TravelNodePathType):
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this // walk(1) — Walk via navmesh waypoints (stored in DB)
// requires a locking mechanism. // areaTrigger(2) — AreaTrigger teleport (auto-discovered at startup)
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
// flightPath(4) — Taxi flight between flight masters
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
//
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
// a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at
// startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock
// caused 100-250ms contention spikes against bot threads.
// //
// Initially the current nodes have been made: // Initially the current nodes have been made:
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route // Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in // calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with // instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators // teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is // (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
// good for global coverage) // good for global coverage).
// //
// To increase coverage/linking extra nodes can be automatically be created. // To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly. // console command after importing the specified node. Current implementation places nodes on paths (including
// After calculating possible links the node is removed if it does not create local coverage. // complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
// does not create local coverage (.fullgenerate only).
// //
// Travel Flow:
//
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
//
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
// as a fallback.
//
// The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item.
//
// Thread Safety:
//
// The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still
// exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no
// runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup
// (saveNodeStore) and by the debug dump command.
//
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
enum class TravelNodePathType : uint8 enum class TravelNodePathType : uint8
{ {
none = 0, none = 0,
walk = 1, walk = 1,
portal = 2, areaTrigger = 2,
transport = 3, transport = 3,
flightPath = 4, flightPath = 4,
teleportSpell = 5 // value 5 (teleportSpell) reserved — no generator emits it and no
// consumer handles it. Re-add when a teleport-spell edge generator
// / executor handler returns.
staticPortal = 6
}; };
// A connection between two nodes. // A connection between two nodes.
class TravelNodePath class TravelNodePath
{ {
public: public:
// Legacy Constructor for travelnodestore
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
// float swimDistance1 = 0, bool flightPath1 = false);
// Constructor // Constructor
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk, TravelNodePath(float distance = 0.1f, float extraCost = 0,
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0}, uint8 pathType = (uint8)TravelNodePathType::walk,
uint32 pathObject = 0, bool calculated = false,
std::vector<uint8> maxLevelCreature = {0, 0, 0},
float swimDistance = 0) float swimDistance = 0)
: extraCost(extraCost), : extraCost(extraCost),
calculated(calculated), calculated(calculated),
@ -85,7 +119,7 @@ public:
{ {
if (pathType != (uint8)TravelNodePathType::walk) if (pathType != (uint8)TravelNodePathType::walk)
complete = true; complete = true;
}; }
TravelNodePath(TravelNodePath* basePath) TravelNodePath(TravelNodePath* basePath)
{ {
@ -98,11 +132,11 @@ public:
swimDistance = basePath->swimDistance; swimDistance = basePath->swimDistance;
pathType = basePath->pathType; pathType = basePath->pathType;
pathObject = basePath->pathObject; pathObject = basePath->pathObject;
}; }
// Getters // Getters
bool getComplete() { return complete || pathType != TravelNodePathType::walk; } bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
std::vector<WorldPosition> getPath() { return path; } std::vector<WorldPosition> GetPath() { return path; }
TravelNodePathType getPathType() { return pathType; } TravelNodePathType getPathType() { return pathType; }
uint32 getPathObject() { return pathObject; } uint32 getPathObject() { return pathObject; }
@ -130,9 +164,6 @@ public:
extraCost = distance / speed; extraCost = distance / speed;
} }
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
// void setTransport(bool transport1) { transport = transport1; }
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; } void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; } void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
@ -186,9 +217,10 @@ class TravelNode
{ {
public: public:
// Constructors // Constructors
TravelNode(){}; TravelNode() {}
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false) TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
bool important1 = false)
{ {
nodeName = nodeName1; nodeName = nodeName1;
point = point1; point = point1;
@ -207,11 +239,11 @@ public:
void setPoint(WorldPosition point1) { point = point1; } void setPoint(WorldPosition point1) { point = point1; }
// Getters // Getters
std::string const getName() { return nodeName; }; std::string const getName() { return nodeName; }
WorldPosition* getPosition() { return &point; }; WorldPosition* getPosition() { return &point; }
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; } std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; } std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
bool isImportant() { return important; }; bool isImportant() { return important; }
bool isLinked() { return linked; } bool isLinked() { return linked; }
bool isTransport() bool isTransport()
@ -235,9 +267,9 @@ public:
bool isPortal() bool isPortal()
{ {
for (auto const& link : *getLinks()) for (auto const& link : *getLinks())
if (link.second->getPathType() == TravelNodePathType::portal) if (link.second->getPathType() == TravelNodePathType::areaTrigger ||
link.second->getPathType() == TravelNodePathType::staticPortal)
return true; return true;
return false; return false;
} }
@ -251,17 +283,25 @@ public:
} }
// WorldLocation shortcuts // WorldLocation shortcuts
uint32 getMapId() { return point.GetMapId(); } uint32 GetMapId() { return point.GetMapId(); }
float getX() { return point.GetPositionX(); } float getX() { return point.GetPositionX(); }
float getY() { return point.GetPositionY(); } float getY() { return point.GetPositionY(); }
float getZ() { return point.GetPositionZ(); } float getZ() { return point.GetPositionZ(); }
float getO() { return point.GetOrientation(); } float getO() { return point.GetOrientation(); }
float getDistance(WorldPosition pos) { return point.distance(pos); } float getDistance(WorldPosition pos) { return point.distance(pos); }
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); } float getDistance(TravelNode* node)
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); } {
return point.distance(node->getPosition());
}
float fDist(TravelNode* node)
{
return point.fDist(node->getPosition());
}
float fDist(WorldPosition pos) { return point.fDist(pos); } float fDist(WorldPosition pos) { return point.fDist(pos); }
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true) TravelNodePath* setPathTo(TravelNode* node,
TravelNodePath path = TravelNodePath(),
bool isLink = true)
{ {
if (this != node) if (this != node)
{ {
@ -275,10 +315,20 @@ public:
return nullptr; return nullptr;
} }
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); } bool hasPathTo(TravelNode* node)
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; } {
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); } return paths.find(node) != paths.end();
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false); }
TravelNodePath* getPathTo(TravelNode* node)
{
return &paths[node];
}
bool hasCompletePathTo(TravelNode* node)
{
return hasPathTo(node) && getPathTo(node)->getComplete();
}
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
bool postProcess = false);
void setLinkTo(TravelNode* node, float distance = 0.1f) void setLinkTo(TravelNode* node, float distance = 0.1f)
{ {
@ -291,9 +341,18 @@ public:
} }
} }
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); } bool hasLinkTo(TravelNode* node)
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } {
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } return links.find(node) != links.end();
}
float linkCostTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
float linkDistanceTo(TravelNode* node)
{
return paths.find(node)->second.getDistance();
}
void removeLinkTo(TravelNode* node, bool removePaths = false); void removeLinkTo(TravelNode* node, bool removePaths = false);
bool isEqual(TravelNode* compareNode); bool isEqual(TravelNode* compareNode);
@ -304,7 +363,8 @@ public:
bool cropUselessLinks(); bool cropUselessLinks();
// Returns all nodes that can be reached from this node. // Returns all nodes that can be reached from this node.
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {}); std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
std::vector<TravelNode*> ignoreNodes = {});
// Checks if it is even possible to route to this node. // Checks if it is even possible to route to this node.
bool hasRouteTo(TravelNode* node) bool hasRouteTo(TravelNode* node)
@ -314,7 +374,10 @@ public:
routes[mNode] = true; routes[mNode] = true;
return routes.find(node) != routes.end(); return routes.find(node) != routes.end();
}; }
void clearRoutes() { routes.clear(); }
void setRouteTo(TravelNode* node) { routes[node] = true; }
void print(bool printFailed = true); void print(bool printFailed = true);
@ -344,63 +407,66 @@ protected:
// uint32 transportId = 0; // uint32 transportId = 0;
}; };
class PortalNode : public TravelNode
{
public:
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
{
nodeName = baseNode->getName();
point = *baseNode->getPosition();
paths.clear();
links.clear();
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
setPathTo(endNode, path);
};
};
// Route step type // Route step type
enum PathNodeType enum class PathNodeType : uint8
{ {
NODE_PREPATH = 0, NODE_PREPATH = 0,
NODE_PATH = 1, NODE_PATH = 1,
NODE_NODE = 2, NODE_NODE = 2,
NODE_PORTAL = 3, NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4, NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5, NODE_FLIGHTPATH = 5,
NODE_TELEPORT = 6 // value 6 (NODE_TELEPORT) reserved — no consumer; re-add when a
// teleport-spell handler / generator returns.
NODE_STATIC_PORTAL = 7
}; };
struct PathNodePoint struct PathNodePoint
{ {
WorldPosition point; WorldPosition point;
PathNodeType type = NODE_PATH; PathNodeType type = PathNodeType::NODE_PATH;
uint32 entry = 0; uint32 entry = 0;
bool operator==(const PathNodePoint& p1) const
{
return point == p1.point && type == p1.type && entry == p1.entry;
}
// A "walkable" node is one we traverse on foot. Portals/transports/
// taxis/teleports are entry/exit hops, not points to anchor a
// shortcut on. Used by makeShortCut to skip them when picking the
// closest-point-on-path to the bot.
bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; }
}; };
// A complete list of points the bots has to walk to or teleport to. // A complete list of points the bots has to walk to or teleport to.
class TravelPath class TravelPath
{ {
public: public:
TravelPath(){}; TravelPath() {}
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; } TravelPath(std::vector<PathNodePoint> fullPath1)
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0) {
fullPath = fullPath1;
}
TravelPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
addPath(path, type, entry); addPath(path, type, entry);
} }
void addPoint(PathNodePoint point) { fullPath.push_back(point); } void addPoint(PathNodePoint point) { fullPath.push_back(point); }
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0) void addPoint(WorldPosition point,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
fullPath.push_back(PathNodePoint{point, type, entry}); fullPath.push_back(PathNodePoint{point, type, entry});
} }
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0) void addPath(std::vector<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{ {
for (auto& p : path) for (auto& p : path)
{
fullPath.push_back(PathNodePoint{p, type, entry}); fullPath.push_back(PathNodePoint{p, type, entry});
};
} }
void addPath(std::vector<PathNodePoint> newPath) void addPath(std::vector<PathNodePoint> newPath)
{ {
@ -408,8 +474,11 @@ public:
} }
void clear() { fullPath.clear(); } void clear() { fullPath.clear(); }
bool empty() { return fullPath.empty(); } bool empty() const { return fullPath.empty(); }
std::vector<PathNodePoint> getPath() { return fullPath; } size_t size() const { return fullPath.size(); }
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
std::vector<PathNodePoint> GetPath() { return fullPath; }
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
WorldPosition getFront() { return fullPath.front().point; } WorldPosition getFront() { return fullPath.front().point; }
WorldPosition getBack() { return fullPath.back().point; } WorldPosition getBack() { return fullPath.back().point; }
@ -419,13 +488,14 @@ public:
for (auto const& p : fullPath) for (auto const& p : fullPath)
retVec.push_back(p.point); retVec.push_back(p.point);
return retVec; return retVec;
}; }
bool makeShortCut(WorldPosition startPos, float maxDist); bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p, // Reject paths the navmesh accepts but a player can't walk:
float& moveDist, float maxDist); // 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1.
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry); static bool IsPathCheating(std::vector<WorldPosition> const& path,
float endpointDistance);
std::ostringstream const print(); std::ostringstream const print();
@ -438,16 +508,24 @@ class TravelNodeRoute
{ {
public: public:
TravelNodeRoute() {} TravelNodeRoute() {}
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ } TravelNodeRoute(std::vector<TravelNode*> nodes1)
{
nodes = nodes1;
}
bool isEmpty() { return nodes.empty(); } bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); } bool hasNode(TravelNode* node)
{
return findNode(node) != nodes.end();
}
float getTotalDistance(); float getTotalDistance();
std::vector<TravelNode*> getNodes() { return nodes; } std::vector<TravelNode*> getNodes() { return nodes; }
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {}, TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {},
Unit* bot = nullptr); Unit* bot = nullptr);
std::ostringstream const print(); std::ostringstream const print();
@ -467,12 +545,43 @@ public:
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; } TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
TravelNode* dataNode; TravelNode* dataNode;
float m_f = 0.0, m_g = 0.0, m_h = 0.0; float totalCost = 0.0;
bool open = false, close = false; float costFromStart = 0.0;
float heuristic = 0.0;
bool open = false;
bool closed = false;
TravelNodeStub* parent = nullptr; TravelNodeStub* parent = nullptr;
uint32 currentGold = 0; uint32 currentGold = 0;
}; };
struct TravelPlan
{
WorldPosition destination;
// Flat waypoint path built upfront by GetFullPath:
TravelPath steps;
uint32 stepIdx{0};
// Spline scratch (used by executor):
std::vector<G3D::Vector3> walkPoints;
uint32 expectedDuration{0}; // used to derive the lastMove delay
// Taxi scratch:
std::vector<uint32> route;
bool IsActive() const { return !steps.empty(); }
void Reset()
{
destination = WorldPosition();
steps.clear();
stepIdx = 0;
walkPoints.clear();
expectedDuration = 0;
route.clear();
}
};
// The container of all nodes. // The container of all nodes.
class TravelNodeMap class TravelNodeMap
{ {
@ -484,14 +593,18 @@ public:
return instance; return instance;
} }
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false, TravelNode* addNode(WorldPosition pos,
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0); std::string const preferedName = "Travel Node",
bool isImportant = false,
bool checkDuplicate = true,
bool transport = false,
uint32 transportId = 0);
void removeNode(TravelNode* node); void removeNode(TravelNode* node);
bool removeNodes() bool removeNodes()
{ {
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10))) if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
{ {
for (auto& node : m_nodes) for (auto& node : nodes)
removeNode(node); removeNode(node);
m_nMapMtx.unlock(); m_nMapMtx.unlock();
@ -499,28 +612,32 @@ public:
} }
return false; return false;
}; }
void fullLinkNode(TravelNode* startNode, Unit* bot); void fullLinkNode(TravelNode* startNode, Unit* bot);
// Get all nodes // Get all nodes
std::vector<TravelNode*> getNodes() { return m_nodes; } std::vector<TravelNode*> getNodes() { return nodes; }
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1); std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
// Find nearest node. // Find nearest node.
TravelNode* getNode(TravelNode* sameNode) TravelNode* getNode(TravelNode* sameNode)
{ {
for (auto& node : m_nodes) for (auto& node : nodes)
{ {
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition()) if (node->getName() == sameNode->getName()
&& node->getPosition() == sameNode->getPosition())
return node; return node;
} }
return nullptr; return nullptr;
} }
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1); TravelNode* getNode(WorldPosition pos,
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1) std::vector<WorldPosition>& ppath,
Unit* bot = nullptr, float range = -1);
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
float range = -1)
{ {
std::vector<WorldPosition> ppath; std::vector<WorldPosition> ppath;
return getNode(pos, ppath, bot, range); return getNode(pos, ppath, bot, range);
@ -536,19 +653,17 @@ public:
return rNodes[urand(0, rNodes.size() - 1)]; return rNodes[urand(0, rNodes.size() - 1)];
} }
// Finds the best nodePath between two nodes // Finds the best nodePath between two nodes (A* over the node graph)
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr); TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
Player* bot);
// Find the best node between two positions // Picks the nearest start/end nodes for two world positions and runs A*
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath, // over the node graph to return a full route between them.
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
WorldPosition endPos,
std::vector<WorldPosition>& startPath,
Player* bot = nullptr); Player* bot = nullptr);
// Find the full path between those locations
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
// Manage/update nodes
void manageNodes(Unit* bot, bool mapFull = false);
void setHasToGen() { hasToGen = true; } void setHasToGen() { hasToGen = true; }
void generateNpcNodes(); void generateNpcNodes();
@ -563,15 +678,17 @@ public:
void removeUselessPaths(); void removeUselessPaths();
void calculatePathCosts(); void calculatePathCosts();
void generateTaxiPaths(); void generateTaxiPaths();
void generatePaths(); void generatePaths(bool fullGen = false);
void generateAll(); void generateAll();
void Init();
void printMap(); void printMap();
void printNodeStore(); void printNodeStore();
void saveNodeStore(); void saveNodeStore();
void loadNodeStore(); void LoadNodeStore();
bool cropUselessNode(TravelNode* startNode); bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode); TravelNode* addZoneLinkNode(TravelNode* startNode);
@ -584,8 +701,28 @@ public:
void InitTaxiGraph(); void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode); std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
void BuildZoneIndex();
void PrecomputeReachability();
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
// All nodes registered to a zone (post-BuildZoneIndex). Returns an
// empty static vector for unknown zones.
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr);
// Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
WorldPosition endPos);
// Get stored walk points for one edge (from→to). Empty if no path.
std::vector<G3D::Vector3> GetEdgeWalkPoints(TravelNode* from,
TravelNode* to);
std::shared_timed_mutex m_nMapMtx; std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
private: private:
TravelNodeMap() = default; TravelNodeMap() = default;
@ -601,13 +738,18 @@ private:
void BuildTaxiGraph(); void BuildTaxiGraph();
void ComputeAllPaths(); void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode); std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode, std::vector<uint32> BuildPath(
uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap); const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph; std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache; std::map<uint32, std::map<uint32, std::vector<uint32>>>
m_taxiPathCache;
std::vector<TravelNode*> m_nodes; std::vector<TravelNode*> nodes;
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets; std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -72,7 +72,6 @@ bool PlayerbotAIConfig::Initialize()
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500); globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000); maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0); disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
maxMovementSearchTime = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxMovementSearchTime", 3);
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000); expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700); dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100); reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
@ -84,8 +83,6 @@ bool PlayerbotAIConfig::Initialize()
sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000); sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000);
returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000); returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000);
lootDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.LootDelay", 1000); lootDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.LootDelay", 1000);
minBotsForGreaterBuff = sConfigMgr->GetOption<int32>("AiPlayerbot.MinBotsForGreaterBuff", 3);
rpWarningCooldown = sConfigMgr->GetOption<int32>("AiPlayerbot.RPWarningCooldown", 30);
disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30);
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
@ -116,6 +113,32 @@ bool PlayerbotAIConfig::Initialize()
highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65); highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65);
autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true); autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true);
saveManaThreshold = sConfigMgr->GetOption<int32>("AiPlayerbot.SaveManaThreshold", 60); saveManaThreshold = sConfigMgr->GetOption<int32>("AiPlayerbot.SaveManaThreshold", 60);
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoGreaterBlessings", 1))
{
case 0:
autoGreaterBlessings = AutoPartyBuffMode::DISABLED;
break;
case 2:
autoGreaterBlessings = AutoPartyBuffMode::GROUP_OR_RAID;
break;
case 1:
default:
autoGreaterBlessings = AutoPartyBuffMode::RAID_ONLY;
break;
}
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoPartyBuffs", 2))
{
case 0:
autoPartyBuffs = AutoPartyBuffMode::DISABLED;
break;
case 1:
autoPartyBuffs = AutoPartyBuffMode::RAID_ONLY;
break;
case 2:
default:
autoPartyBuffs = AutoPartyBuffMode::GROUP_OR_RAID;
break;
}
autoAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoAvoidAoe", true); autoAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoAvoidAoe", true);
maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f); maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f);
LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"), LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"),
@ -654,6 +677,7 @@ bool PlayerbotAIConfig::Initialize()
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false); autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true); autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true); enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15); RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20); RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);

View File

@ -40,6 +40,13 @@ enum class HealingManaEfficiency : uint8
SUPERIOR = 32 SUPERIOR = 32
}; };
enum class AutoPartyBuffMode : uint8
{
DISABLED = 0,
RAID_ONLY = 1,
GROUP_OR_RAID = 2
};
enum NewRpgStatus : int enum NewRpgStatus : int
{ {
//Initial Status //Initial Status
@ -84,7 +91,7 @@ public:
bool EnableICCBuffs; bool EnableICCBuffs;
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots; bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat; bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime, uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay; dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
bool dynamicReactDelay; bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
@ -94,6 +101,8 @@ public:
uint32 lowMana, mediumMana, highMana; uint32 lowMana, mediumMana, highMana;
bool autoSaveMana; bool autoSaveMana;
uint32 saveManaThreshold; uint32 saveManaThreshold;
AutoPartyBuffMode autoGreaterBlessings;
AutoPartyBuffMode autoPartyBuffs;
bool autoAvoidAoe; bool autoAvoidAoe;
float maxAoeAvoidRadius; float maxAoeAvoidRadius;
std::set<uint32> aoeAvoidSpellWhitelist; std::set<uint32> aoeAvoidSpellWhitelist;
@ -146,12 +155,6 @@ public:
uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay; uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay;
bool randomBotJoinLfg; bool randomBotJoinLfg;
// Buff system
// Min group size to use Greater buffs (Paladin, Mage, Druid). Default: 3
int32 minBotsForGreaterBuff;
// Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30
int32 rpWarningCooldown;
// Professions // Professions
bool enableFishingWithMaster; bool enableFishingWithMaster;
uint32 classMatchingProfessionChance; uint32 classMatchingProfessionChance;
@ -372,6 +375,7 @@ public:
bool autoLearnTrainerSpells; bool autoLearnTrainerSpells;
bool autoDoQuests; bool autoDoQuests;
bool enableNewRpgStrategy; bool enableNewRpgStrategy;
bool enableTravelNodes;
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight; std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
bool syncLevelWithPlayers; bool syncLevelWithPlayers;
bool autoLearnQuestSpells; bool autoLearnQuestSpells;

View File

@ -20,6 +20,7 @@
#include "PlayerbotMgr.h" #include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h" #include "ScriptMgr.h"
#include "TravelNode.h"
using namespace Acore::ChatCommands; using namespace Acore::ChatCommands;
@ -32,6 +33,7 @@ public:
{ {
static ChatCommandTable playerbotsDebugCommandTable = { static ChatCommandTable playerbotsDebugCommandTable = {
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes}, {"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
{"zone", HandleDebugZoneCommand, SEC_GAMEMASTER, Console::No},
}; };
static ChatCommandTable playerbotsAccountCommandTable = { static ChatCommandTable playerbotsAccountCommandTable = {
@ -41,11 +43,16 @@ public:
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No}, {"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
}; };
static ChatCommandTable playerbotsTravelCommandTable = {
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
};
static ChatCommandTable playerbotsCommandTable = { static ChatCommandTable playerbotsCommandTable = {
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No}, {"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes}, {"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes}, {"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes}, {"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
{"travel", playerbotsTravelCommandTable},
{"debug", playerbotsDebugCommandTable}, {"debug", playerbotsDebugCommandTable},
{"account", playerbotsAccountCommandTable}, {"account", playerbotsAccountCommandTable},
}; };
@ -106,11 +113,177 @@ public:
return true; return true;
} }
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
{
handler->PSendSysMessage("Regenerating travel node paths...");
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
sTravelNodeMap.generateAll();
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
return true;
}
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args) static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
{ {
return BGTactics::HandleConsoleCommand(handler, args); return BGTactics::HandleConsoleCommand(handler, args);
} }
// Visual constants for showpath markers. Two waypoint-family
// creatures give nodes vs path waypoints distinct visuals; both
// render at their creature_template default scale (no override).
// nodes (anchors) → 15897, prominent waypoint variant
// path waypoints → 15631, standard BG-showpath waypoint
//
// SHOWPATH_PATH_DISPLAY_ID = 0 uses the path-creature's default
// model. To experiment with a model override, set this to a known-
// good creature display ID for your DB (spell-visual IDs are not
// universally registered as creature displays — using one risks
// summoning invisible markers).
static constexpr uint32 SHOWPATH_NODE_CREATURE = 15897;
static constexpr uint32 SHOWPATH_PATH_CREATURE = 15631;
static constexpr uint32 SHOWPATH_PATH_DISPLAY_ID = 0; // 0 = default model
static constexpr uint32 SHOWPATH_DESPAWN_MS = 60000;
static bool HandleDebugZoneCommand(ChatHandler* handler, char const* args)
{
Player* player = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
if (!player)
{
handler->PSendSysMessage("Command requires an in-game player.");
return false;
}
if (!args || !*args)
{
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
return false;
}
char* cmd = strtok(const_cast<char*>(args), " ");
// showpath=all → nodes + cached path waypoints (full picture)
// showpath=node → only node anchors
// showpath=path → only cached path waypoints (no anchors)
bool showNodes = false;
bool showLinks = false;
if (cmd && strcmp(cmd, "showpath=all") == 0)
{
showNodes = true;
showLinks = true;
}
else if (cmd && strcmp(cmd, "showpath=node") == 0)
{
showNodes = true;
showLinks = false;
}
else if (cmd && strcmp(cmd, "showpath=path") == 0)
{
showNodes = false;
showLinks = true;
}
else
{
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
return false;
}
uint32 zoneId = player->GetZoneId();
std::vector<TravelNode*> const& nodes = sTravelNodeMap.GetNodesInZone(zoneId);
if (nodes.empty())
{
handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId);
return true;
}
// node markers — full-scale anchor at each travel-node position.
uint32 nodesPlaced = 0;
if (showNodes)
{
for (TravelNode* node : nodes)
{
if (!node)
continue;
WorldPosition* pos = node->getPosition();
if (!pos || pos->GetMapId() != player->GetMapId())
continue;
Creature* wp = player->SummonCreature(SHOWPATH_NODE_CREATURE,
pos->GetPositionX(), pos->GetPositionY(),
pos->GetPositionZ(), 0,
TEMPSUMMON_TIMED_DESPAWN, SHOWPATH_DESPAWN_MS);
if (wp)
{
wp->SetOwnerGUID(player->GetGUID());
++nodesPlaced;
}
}
}
if (!showLinks)
{
handler->PSendSysMessage("Showing {} travel nodes in zone {} (60s)", nodesPlaced, zoneId);
return true;
}
// path-waypoint markers — same creature, scaled down so they
// read as a breadcrumb trail between nodes rather than as more
// anchor points. Walk-type links from any in-zone node are
// drawn; the per-waypoint same-map filter keeps the trail from
// running into other continents. Sparse zones (e.g. Teldrassil)
// would draw nothing if we required dst-in-zone too, since their
// only links go to nodes in neighbouring zones.
constexpr uint32 MAX_PATH_MARKERS = 500;
uint32 pathPlaced = 0;
uint32 linksDrawn = 0;
bool capped = false;
for (TravelNode* node : nodes)
{
if (!node)
continue;
auto* links = node->getLinks();
if (!links)
continue;
for (auto const& kv : *links)
{
TravelNode* dst = kv.first;
TravelNodePath* path = kv.second;
if (!dst || !path)
continue;
if (path->getPathType() != TravelNodePathType::walk)
continue;
++linksDrawn;
for (WorldPosition const& wpPos : path->GetPath())
{
if (wpPos.GetMapId() != player->GetMapId())
continue;
if (pathPlaced >= MAX_PATH_MARKERS)
{
capped = true;
break;
}
Creature* mk = player->SummonCreature(SHOWPATH_PATH_CREATURE,
wpPos.GetPositionX(),
wpPos.GetPositionY(), wpPos.GetPositionZ(),
0, TEMPSUMMON_TIMED_DESPAWN,
SHOWPATH_DESPAWN_MS);
if (mk)
{
mk->SetOwnerGUID(player->GetGUID());
if (SHOWPATH_PATH_DISPLAY_ID)
mk->SetDisplayId(SHOWPATH_PATH_DISPLAY_ID);
++pathPlaced;
}
}
if (capped)
break;
}
if (capped)
break;
}
handler->PSendSysMessage("Showing {} nodes + {} path waypoints across {} walk links in zone {}{} (60s)",
nodesPlaced, pathPlaced, linksDrawn, zoneId,
capped ? " — capped at 500 path markers" : "");
return true;
}
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args) static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
{ {
if (!args || !*args) if (!args || !*args)

24
todo.md Normal file
View File

@ -0,0 +1,24 @@
Aligned:
Bot 50° cap + water 10× bias at engine filter level (covers every path query)
PathFinder reuse + Clear() per step
Underwater path-extension + dispatch fixup
GetFullPath mmap-probe-first
BG gating
ClipPath (LOS + level+5)
Inactive-bot teleport (with self-bot carve-out — intentional)
masterWalking
Pre-dispatch state cleanup
setPath before mutations
needsLongPath + cross-map gate
StopMoving short-stop
10% reuse + regression guard
WaitForReach formula
Stateless re-resolve
walkDistance config
Differ (blocked on infrastructure):
Hazard avoidance (GeneratePathAvoidingHazards) — needs hazard system
Out of scope:
Flying / transports / vehicles