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
48 changed files with 2764 additions and 1407 deletions

View File

@ -22,7 +22,6 @@
# THRESHOLDS
# QUESTS
# COMBAT
# GREATER BUFFS STRATEGIES
# CHEATS
# SPELLS
# FLIGHTPATH
@ -474,6 +473,23 @@ AiPlayerbot.AutoSaveMana = 1
# Default: 60 (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
AiPlayerbot.FleeingEnabled = 1
@ -482,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
#

View File

@ -95,9 +95,8 @@ bool FollowAction::Execute(Event /*event*/)
bool const movingAllowed = IsMovingAllowed();
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
bool const waiting = IsWaitingForLastMove(priority);
if (movingAllowed && !dupMove && !waiting)
if (movingAllowed && !dupMove)
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);

View File

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

View File

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

View File

@ -215,9 +215,6 @@ bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPri
if (IsDuplicateMove(x, y, z))
return false;
if (IsWaitingForLastMove(priority))
return false;
float speed = bot->GetSpeed(MOVE_RUN);
MotionMaster& mm = *bot->GetMotionMaster();
mm.Clear();
@ -325,10 +322,6 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
{
return false;
}
if (IsWaitingForLastMove(priority))
{
return false;
}
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
bool disableMoveSplinePath =
@ -1053,20 +1046,6 @@ bool MovementAction::IsDuplicateMove(float x, float y, float z)
return true;
}
bool MovementAction::IsWaitingForLastMove(MovementPriority priority)
{
LastMovement& lastMove = *context->GetValue<LastMovement&>("last movement");
if (priority > lastMove.priority)
return false;
// heuristic 5s
if (lastMove.lastdelayTime + lastMove.msTime > getMSTime())
return true;
return false;
}
bool MovementAction::IsMovingAllowed()
{
return botAI->CanMove();
@ -1423,9 +1402,7 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
bool MovementAction::ChaseTo(WorldObject* obj, float distance)
{
if (!IsMovingAllowed())
{
return false;
}
if (obj)
EmitDebugMove("ChaseTo", "chase", obj->GetPositionX(), obj->GetPositionY(), obj->GetPositionZ());
@ -1435,8 +1412,6 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance)
VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
if (!seat || !seat->CanControl())
return false;
// vehicle->GetMotionMaster()->Clear();
vehicle->GetBase()->GetMotionMaster()->MoveChase((Unit*)obj, 30.0f);
return true;
}
@ -1452,10 +1427,34 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance)
botAI->InterruptSpell();
}
// bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveChase((Unit*)obj, distance);
// Try a chained mmap probe first — for targets behind obstacles
// this routes the bot around terrain instead of straight-charging
// into a wall. Falls back to engine MoveChase for short/clear
// chases where target tracking matters more than path routing.
float const targetDist = bot->GetExactDist(obj);
if (targetDist > distance + 3.0f)
{
float const angle = obj->GetAngle(bot);
float x = obj->GetPositionX();
float y = obj->GetPositionY();
float z = obj->GetPositionZ();
obj->GetNearPoint(bot, x, y, z, bot->GetObjectSize(), distance, angle);
// TODO shouldnt this use "last movement" value?
PathGenerator path(bot);
path.CalculatePath(x, y, z, false);
PathType type = path.GetPathType();
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_SHORTCUT)) &&
path.GetPath().size() >= 2)
{
Movement::PointsArray points = path.GetPath();
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
WaitForReach(targetDist - distance);
return true;
}
}
bot->GetMotionMaster()->MoveChase((Unit*)obj, distance);
WaitForReach(bot->GetExactDist2d(obj) - distance);
return true;
}
@ -3064,51 +3063,6 @@ bool MoveAwayFromPlayerWithDebuffAction::isPossible()
return bot->CanFreeMove();
}
bool MovementAction::CheckSplineProgress(TravelPlan& state)
{
if (!state.splineActive)
return false;
// walkPoints may have been cleared by a map transfer or external reset
// while the spline was still flagged active; bail out safely.
if (state.walkPoints.empty())
{
state.splineActive = false;
return false;
}
if (bot->movespline->Finalized())
{
G3D::Vector3 const& endPt = state.walkPoints.back();
float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z);
if (distToEnd < 10.0f)
{
state.splineActive = false;
state.walkPoints.clear();
return true; // Arrived
}
// Spline finalized short of target — interrupted (combat/knockback/etc).
// Caller will re-launch.
state.splineActive = false;
return false;
}
// Stuck detection
if (state.splineStartTime &&
GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS))
{
G3D::Vector3 const& endPt = state.walkPoints.back();
botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z));
state.splineActive = false;
state.walkPoints.clear();
return true;
}
return false; // Still moving
}
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
{
if (state.walkPoints.size() < 2)
@ -3190,14 +3144,10 @@ bool MovementAction::LaunchWalkSpline(TravelPlan& state)
bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN);
state.splineStartTime = getMSTime();
state.splineActive = true;
G3D::Vector3 const& last = state.walkPoints.back();
// Update LastMovement so MoveFarTo's spline-active early-out
// knows about this in-flight walk and won't recompute the path
// mid-spline. Mirror what MoveTo does after dispatching a spline.
// Mirror what MoveTo does after dispatching a spline so the
// lastPath cache below picks up the in-flight waypoint chain.
{
float delay = static_cast<float>(state.expectedDuration);
delay = std::min(delay, static_cast<float>(sPlayerbotAIConfig.maxWaitForMove));
@ -3323,19 +3273,6 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
// an executor-ran-this-tick label here would whisper every tick
// while the plan is active.
// Handle active spline
if (state.splineActive)
{
if (!CheckSplineProgress(state))
{
if (state.splineActive)
return true; // Still moving
else
LaunchWalkSpline(state); // Interrupted, re-launch
}
return true;
}
if (state.stepIdx >= state.steps.size())
{
state.Reset();
@ -3347,89 +3284,29 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
switch (pt.type)
{
case PathNodeType::NODE_PREPATH:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.stepIdx++;
return true;
}
float const botX = bot->GetPositionX();
float const botY = bot->GetPositionY();
float const botZ = bot->GetPositionZ();
// Walk forward through the route while distance keeps shrinking.
// Once it starts growing we're past the closest waypoint — break.
size_t bestIdx = state.stepIdx + 1;
float bestDistSq = FLT_MAX;
for (size_t i = state.stepIdx + 1; i < state.steps.size(); ++i)
{
const PathNodePoint& cand = state.steps[i];
if (cand.type != PathNodeType::NODE_PATH &&
cand.type != PathNodeType::NODE_NODE)
break; // stop at portal/transport/etc — can't walk past
float const dx = cand.point.GetPositionX() - botX;
float const dy = cand.point.GetPositionY() - botY;
float const dz = cand.point.GetPositionZ() - botZ;
float const dSq = dx * dx + dy * dy + dz * dz;
if (dSq >= bestDistSq)
break; // moving away — closest waypoint already found
bestDistSq = dSq;
bestIdx = i;
}
constexpr float ARRIVAL_DIST = 5.0f;
WorldPosition const& target = state.steps[bestIdx].point;
float const distToTarget = bot->GetExactDist(
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
if (distToTarget < ARRIVAL_DIST)
{
state.stepIdx = bestIdx;
return true;
}
// Validate the path before MoveTo. PathGenerator can
// return NORMAL | NOT_USING_PATH when start or end poly
// is invalid (BuildShortcut → 2-point straight line).
// PointMovementGenerator would then dispatch the bot
// straight through any geometry between bot and target.
// The default accept mask (NORMAL | INCOMPLETE) rejects
// NOT_USING_PATH, so abort the plan and let MoveFarTo
// re-derive instead of walking a known-bad shortcut.
PathResult validate = GeneratePath(
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
DEFAULT_PATH_ACCEPT_MASK, false);
if (!validate.reachable)
{
EmitDebugMove("TravelPlan", "prepath-unreachable",
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
state.Reset();
return false;
}
return MoveTo(target.GetMapId(),
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
false, false, false, true /*exact_waypoint*/);
}
case PathNodeType::NODE_PATH:
case PathNodeType::NODE_NODE:
{
// Batch consecutive walk points into one spline. Capped at
// 20 points per dispatch as a cheap upper bound on per-tick
// work; stepIdx advances exactly in step with what's
// dispatched, so the next tick picks up from the cutoff.
// Batch consecutive walkable points (PREPATH, PATH, NODE) into
// one spline. With per-tick re-resolve the plan starts at
// stepIdx=0 each tick, so we must dispatch a real spline (not
// a single-waypoint MoveTo) — otherwise the executor's
// "near closest waypoint" heuristic returns true without
// moving and the bot never advances.
//
// Capped at 20 points per dispatch as a cheap upper bound on
// per-tick work. The engine plays the spline; next tick
// re-resolves from the bot's new position and dispatches the
// next batch.
static constexpr uint32 MAX_SPLINE_POINTS = 20;
state.walkPoints.clear();
while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS)
{
const PathNodePoint& wp = state.steps[state.stepIdx];
if (wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE)
break;
if (wp.type != PathNodeType::NODE_PREPATH &&
wp.type != PathNodeType::NODE_PATH &&
wp.type != PathNodeType::NODE_NODE)
break; // stop at portal/transport/etc — can't walk past
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(),
wp.point.GetPositionY(), wp.point.GetPositionZ()));
state.stepIdx++;
@ -3488,32 +3365,102 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
return true;
}
case PathNodeType::NODE_PORTAL:
case PathNodeType::NODE_AREA_TRIGGER:
{
// Pair: source (pointIdx) + dest (pointIdx+1)
// Pair: trigger (pointIdx) + dest (pointIdx+1).
// Bot walks into the area trigger volume; server teleports
// on entry. Bot may need quest/key prereqs to actually cross.
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& src = state.steps[state.stepIdx];
const PathNodePoint& trigger = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
// Already on destination map?
// Already on destination map — trigger fired, advance.
if (bot->GetMapId() == dst.point.GetMapId())
{
state.stepIdx += 2;
return true;
}
// Walk to portal source
float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
// At portal but didn't cross — natural collision missed.
// Abort the plan; stuck-recovery in MoveFarTo will decide
// whether to retry or teleport the bot.
// Walk to the trigger position; collision with the trigger
// volume teleports us.
float dist = bot->GetExactDist(trigger.point.GetPositionX(),
trigger.point.GetPositionY(),
trigger.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(trigger.point.GetMapId(),
trigger.point.GetPositionX(),
trigger.point.GetPositionY(),
trigger.point.GetPositionZ());
// At trigger but didn't teleport — likely missing quest/key.
// Abort; the do-quest yield-to-grind multiplier or next
// POI pick can reroute.
state.Reset();
return false;
}
case PathNodeType::NODE_STATIC_PORTAL:
{
// Pair: portal-GO position (pointIdx) + dest (pointIdx+1).
// Bot walks within interact range of the portal GameObject
// and sends CMSG_GAMEOBJ_USE to trigger its teleport spell.
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& portal = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
if (bot->GetMapId() == dst.point.GetMapId())
{
state.stepIdx += 2;
return true;
}
// Walk to portal GO position
float dist = bot->GetExactDist(portal.point.GetPositionX(),
portal.point.GetPositionY(),
portal.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(portal.point.GetMapId(),
portal.point.GetPositionX(),
portal.point.GetPositionY(),
portal.point.GetPositionZ());
// In range — find the portal GameObject and interact
if (!portal.entry)
{
state.Reset();
return false;
}
if (bot->IsMounted())
bot->Dismount();
botAI->RemoveShapeshift();
GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects");
for (ObjectGuid const& guid : nearGOs)
{
GameObject* go = botAI->GetGameObject(guid);
if (!go || go->GetEntry() != portal.entry)
continue;
if (!bot->GetGameObjectIfCanInteractWith(guid, GAMEOBJECT_TYPE_SPELLCASTER))
continue;
WorldPacket packet(CMSG_GAMEOBJ_USE);
packet << guid;
bot->GetSession()->QueuePacket(new WorldPacket(packet));
return true;
}
// GO not found nearby — abort and let next tick try again
state.Reset();
return false;
}
@ -3637,24 +3584,6 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
return true;
}
case PathNodeType::NODE_TELEPORT:
{
// Teleport-spell node (e.g. mage portals). Not implemented
// — abort the plan instead of silently teleporting the
// bot. The plan executor regards this node as terminal.
state.Reset();
return false;
}
case PathNodeType::NODE_FLYING_MOUNT:
{
// Flying-mount node not implemented — abort. The graph
// generator produces these but their execution is
// server-specific; we treat them as unreachable rather
// than papering over with a teleport.
state.Reset();
return false;
}
default:
{
LOG_ERROR("playerbots",

View File

@ -72,7 +72,6 @@ protected:
void SetNextMovementDelay(float delayMillis);
bool IsMovingAllowed(WorldObject* target);
bool IsDuplicateMove(float x, float y, float z);
bool IsWaitingForLastMove(MovementPriority priority);
bool IsMovingAllowed();
bool Flee(Unit* target);
void ClearIdleState();
@ -104,7 +103,6 @@ protected:
private:
bool LaunchWalkSpline(TravelPlan& state);
bool CheckSplineProgress(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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,6 @@
#include "AiObject.h"
#include "GenericSpellActions.h"
#include "SharedDefines.h"
class PlayerbotAI;
class Unit;
// seals
BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness");
@ -88,24 +84,13 @@ public:
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
{
public:
CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {}
std::string const getName() override { return "blessing of might on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
@ -124,6 +109,7 @@ public:
CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {}
std::string const getName() override { return "blessing of wisdom on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
@ -134,12 +120,13 @@ public:
CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {}
};
class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction
class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction
{
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"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority
bool Execute(Event event) override; // added for 2 paladins logic
};
@ -156,6 +143,7 @@ public:
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
std::string const getName() override { return "blessing of sanctuary on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() 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 "GenericPaladinNonCombatStrategy.h"
#include "PaladinGreaterBlessingAction.h"
#include "HealPaladinStrategy.h"
#include "NamedObjectContext.h"
#include "OffhealRetPaladinStrategy.h"
@ -70,17 +71,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
public:
PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
{
creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth;
creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana;
creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps;
creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats;
creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc;
creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom;
creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight;
creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings;
}
private:
static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
};
class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -154,6 +155,7 @@ public:
creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party;
creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath;
creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed;
}
private:
@ -211,8 +213,8 @@ private:
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_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); }
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); }
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); }
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); }
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* 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* greater_blessing_needed(PlayerbotAI* botAI)
{
return new GreaterBlessingNeededTrigger(botAI);
}
};
class PaladinAiObjectContextInternal : public NamedObjectContext<Action>
@ -316,6 +322,8 @@ public:
creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice;
creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice;
creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party;
creators["cast greater blessing assignment"] =
&PaladinAiObjectContextInternal::cast_greater_blessing_assignment;
}
private:
@ -414,15 +422,41 @@ private:
static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(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* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); }
static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); }
static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); }
static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); }
static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); }
static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); }
static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); }
static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); }
static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); }
static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); }
static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); }
static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); }
static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); }
static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); }
static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); }
static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); }
static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); }
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;
@ -467,4 +501,5 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext
void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& 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) }));
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("greater blessing needed",
{ NextAction("cast greater blessing assignment", ACTION_NORMAL) }));
}

View File

@ -16,7 +16,7 @@ public:
PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
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
@ -25,7 +25,7 @@ public:
PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
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
@ -34,7 +34,7 @@ public:
PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
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
@ -88,7 +88,7 @@ public:
PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
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

View File

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

View File

@ -5,10 +5,11 @@
#include "PaladinTriggers.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinActions.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "PaladinHelper.h"
#include "Playerbots.h"
bool SealTrigger::IsActive()
{
@ -28,8 +29,9 @@ bool CrusaderAuraTrigger::IsActive()
bool BlessingTrigger::IsActive()
{
Unit* target = GetTarget();
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr);
return SpellTrigger::IsActive() &&
!botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr);
}
bool DivineShieldLowHealthTrigger::IsActive()
@ -62,7 +64,8 @@ bool HandOfFreedomOnPartyTrigger::IsActive()
if (!target)
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;
if (!botAI->CanCastSpell("hand of freedom", target))
@ -75,3 +78,29 @@ bool NotSensingUndeadTrigger::IsActive()
{
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;
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(RighteousFuryTrigger, "righteous fury");
@ -212,42 +186,55 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{
public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI)
: BuffOnMainTankTrigger(botAI, "beacon of light", true) {}
};
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
{
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:
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:
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:
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:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
: BlessingOnPartyTrigger(botAI)
{
spell = "blessing of sanctuary";
}
};
class HandOfFreedomOnPartyTrigger : public Trigger
@ -266,4 +253,13 @@ public:
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

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_FREEDOM = 1044;
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)
{

View File

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

View File

@ -92,8 +92,6 @@ public:
creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection;
creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party;
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["touch of weakness"] = &PriestTriggerFactoryInternal::touch_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(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(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* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); }
static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); }
@ -207,8 +203,6 @@ public:
creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection;
creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party;
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["silence"] = &PriestAiObjectContextInternal::silence;
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* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(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* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(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(
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(
"party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11),
NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) }));
@ -54,12 +56,6 @@ void PriestBuffStrategy::InitTriggers(std::vector<TriggerNode*>& 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(
new TriggerNode("power word: fortitude on party",
{ NextAction("power word: fortitude on party", 11.0f) }));

View File

@ -30,8 +30,6 @@ public:
creators["flash heal"] = &flash_heal;
creators["flash heal on party"] = &flash_heal_on_party;
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:
@ -134,20 +132,6 @@ private:
/*A*/ {},
/*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

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(
new TriggerNode(
"silence",

View File

@ -8,10 +8,9 @@
#include "Player.h"
#include "Playerbots.h"
bool PowerWordFortitudeOnPartyTrigger::IsActive()
bool ShadowProtectionTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) &&
!botAI->HasAura("prayer of fortitude", GetTarget());
return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget());
}
bool PowerWordFortitudeTrigger::IsActive()
@ -20,43 +19,12 @@ bool PowerWordFortitudeTrigger::IsActive()
!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()
{
return BuffTrigger::IsActive() && !botAI->HasAura("divine 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()
{
Unit* target = GetTarget();

View File

@ -27,8 +27,6 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire");
BUFF_TRIGGER_A(ShadowformTrigger, "shadowform");
BOOST_TRIGGER(PowerInfusionTrigger, "power infusion");
BUFF_TRIGGER(InnerFocusTrigger, "inner focus");
BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection");
BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection");
CC_TRIGGER(ShackleUndeadTrigger, "shackle undead");
INTERRUPT_TRIGGER(SilenceTrigger, "silence");
INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence");
@ -44,20 +42,34 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise");
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
{
public:
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000)
{
}
bool IsActive() override;
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {}
};
class PowerWordFortitudeTrigger : public BuffTrigger
{
public:
PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
PowerWordFortitudeTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
bool IsActive() override;
};
@ -65,31 +77,15 @@ public:
class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger
{
public:
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
};
class DivineSpiritTrigger : public BuffTrigger
{
public:
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) {}
DivineSpiritTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
};
@ -106,9 +102,7 @@ class MindSearChannelCheckTrigger : public Trigger
{
public:
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;

View File

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

View File

@ -331,8 +331,11 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{
// yield to attack-anything if a quest mob is right next to us
if (HasNearbyQuestMob(15.0f))
// 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

View File

@ -48,11 +48,19 @@
bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
{
if (dest == WorldPosition())
{
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
return false;
}
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
UpdateMovementState();
if (!IsMovingAllowed())
{
EmitDebugMove("MoveFar", "cant-move",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
// Already-at-dest short-stop. Below targetPosRecalcDistance the
// move is effectively done — stop any active spline and clear
@ -68,26 +76,12 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
lastMove.clear();
}
bot->StopMoving();
EmitDebugMove("MoveFar", "arrived",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
// Let an in-flight spline finish before recomputing — prevents
// oscillation when re-resolve produces a slightly different endpoint.
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
{
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
if (remaining > 10.0f)
{
EmitDebugMove("MoveFar", "spline-plan",
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
return true;
}
}
}
// 10% lastPath reuse — if the cached path's endpoint is still
// close (within 10%) to the new dest, trim the cached path to
// the bot's current position via makeShortCut and re-dispatch.
@ -138,33 +132,35 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
float disToDest = bot->GetDistance(dest);
float dis = bot->GetExactDist(dest);
// Try the travel-node graph first for cross-map or > 50y moves;
// fall back to chained mmap probe otherwise. BGs skip the graph.
constexpr float TRAVELNODE_THRESHOLD = 50.0f;
// Try the travel-node graph for cross-map or moves longer than the
// bot's sight distance; otherwise the chained mmap probe handles it.
// BGs skip the graph.
bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
!bot->InBattleground() &&
((bot->GetMapId() != dest.GetMapId()) ||
(dis > TRAVELNODE_THRESHOLD));
(dis > sPlayerbotAIConfig.sightDistance));
// Ride the active node plan only if its dest still matches.
// A stale plan would steer the bot past a new target.
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
{
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
botAI->rpgInfo.ClearTravel();
else
return UpdateTravelPlan();
}
// PRIORITY: try the travel-node graph FIRST when the move is
// long enough to need it.
// Per-tick re-resolve (cmangos pattern). Rebuild the travel plan
// from the bot's CURRENT position every tick rather than caching
// a multi-step plan and advancing through it. Recovers naturally
// from knockback, off-route drift, mid-execution destination
// changes, and blocked waypoints. Cost: per-tick GetFullPath call;
// the lastPath cache (10% reuse block above) handles the common
// case where the cached path still ends near the same destination
// and avoids re-derivation.
if (tryNodes)
{
if (botAI->rpgInfo.HasActiveTravelPlan() &&
botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
botAI->rpgInfo.ClearTravel();
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
// No `travelplan` label here — per-tick re-resolve calls
// StartTravelPlan every tick, which would whisper-spam.
// The executor emits per-step labels (TravelPlan:walk-start,
// TravelPlan:flight, TravelPlan:transport-*) on actual dispatch.
return UpdateTravelPlan();
}
// Graph returned no plan — fall through to mmap probe.
@ -217,26 +213,10 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
// Walk the chained probe's full waypoint chain via DispatchPathPoints.
if (!probe.empty() && probe.size() >= 2)
{
WorldPosition stepDest = probe.back();
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
stepDest.GetPositionY(), stepDest.GetPositionZ());
float endDistToDest = dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(), probe.back().GetPositionZ());
if (endDistToDest + 5.0f < disToDest)
{
// Z gap check: if the probe's last waypoint is well below
// the requested destination Z, the chain walked the ground
// polygon graph toward an elevated target it can't reach
// (quest giver on top of Aldrassil etc.). Refuse to dispatch
// — bot waits instead of tunneling into the visual model.
// 10y tolerates normal terrain variation (ramp ends, hill
// tops) while still catching clearly unreachable elevations.
if (std::fabs(stepDest.GetPositionZ() - dest.GetPositionZ()) > 10.0f)
{
EmitDebugMove("MoveFar", "z-mismatch",
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
return false;
}
Movement::PointsArray points;
points.reserve(probe.size());
for (auto const& wp : probe)
@ -253,39 +233,23 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
}
}
// Probe failed or didn't progress — emit visibility whisper so
// the user can see WHY mmap didn't dispatch.
{
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
(dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
if (!probeProgressed)
{
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
EmitDebugMove("MoveFar", reason,
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
}
}
// Empty-probe fallback: single-waypoint MoveTo via engine PathGenerator.
// Cross-map can't be served by a single-map spline — bail.
// Probe failed or didn't progress. Attempt straight-line MoveTo to
// the destination — engine PathFinder handles per-poly filtering and
// the bot's STEEP/water filter is honored via CreateFilter. If even
// that fails, the engine falls back to a direct spline.
if (bot->GetMapId() != dest.GetMapId())
return false;
// LOS gate: don't air-walk through trees/walls when the engine
// would otherwise drop to a straight-line BuildShortcut spline.
if (!bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()))
{
EmitDebugMove("MoveFar", "spline-blocked",
EmitDebugMove("MoveFar", "cross-map",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false);
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
EmitDebugMove("MoveFar", reason,
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ(), false, false, false, false);
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
@ -324,52 +288,7 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
return false;
}
// Sparse-segment clip (cmangos parity): if any consecutive segment
// is longer than ~11.18y, truncate the path at that point. Short,
// dense waypoints reduce spline interpolation across visual
// obstacles between sparse points; bot re-plans from a closer
// position next tick.
{
constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y
for (size_t i = 1; i < points.size(); ++i)
{
float dx = points[i].x - points[i - 1].x;
float dy = points[i].y - points[i - 1].y;
float dz = points[i].z - points[i - 1].z;
if (dx * dx + dy * dy + dz * dz > SPARSE_SEG_SQ)
{
points.resize(i);
break;
}
}
if (points.size() < 2)
return false;
}
// LOS gate: reject paths whose segments pass through visual
// geometry. mmap is blind to M2 models (trees, decorative props)
// and will route through them; vmap LOS catches the cases that
// matter — solid trunks, walls, terrain features.
if (Map* map = bot->GetMap())
{
float const eye = bot->GetCollisionHeight();
for (size_t i = 0; i + 1 < points.size(); ++i)
{
if (!map->isInLineOfSight(points[i].x, points[i].y, points[i].z + eye,
points[i + 1].x, points[i + 1].y, points[i + 1].z + eye,
bot->GetPhaseMask(),
LINEOFSIGHT_ALL_CHECKS,
VMAP::ModelIgnoreFlags::Nothing))
{
EmitDebugMove("MoveFar", "blocked",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
}
// Save planner output before clip/fixup so next-tick reuse sees
// the original intent, not a truncated tail.
// Save planner output for next-tick reuse.
{
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
std::vector<WorldPosition> wpts;
@ -379,26 +298,6 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
lm.setPath(TravelPath(wpts));
}
// Underwater fixup: lift submerged waypoints to the surface,
// unless the destination is itself underwater.
if (Map* map = bot->GetMap())
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
for (auto& pt : points)
{
WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
if (wp.isUnderWater())
{
float surface = map->GetWaterLevel(pt.x, pt.y);
if (surface != INVALID_HEIGHT && surface > pt.z)
pt.z = surface;
}
}
}
}
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
@ -474,17 +373,14 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
}
}
// Match master's walk pace when they're nearby and walking.
// Match master's walk pace when they're walking and within 5y.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (sPlayerbotAIConfig.walkDistance > 0.0f)
if (Player* master = botAI->GetMaster())
{
if (Player* master = botAI->GetMaster())
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < 5.0f)
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance)
{
moveMode = FORCED_MOVEMENT_WALK;
}
moveMode = FORCED_MOVEMENT_WALK;
}
}
@ -545,53 +441,28 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
if (!map)
return false;
// 8-angle deterministic iteration around the target. For each angle,
// validate the candidate against the navmesh with a strict ground-only
// filter (NAV_GROUND, exclude STEEP/WATER/MAGMA/SLIME). Reject if no
// valid poly within 5y XY+Z or if the snap drifts the Z by >10y.
// First angle that passes both LOS and navmesh-snap wins.
dtNavMeshQuery const* navMeshQuery =
map->GetMapCollisionData().GetMMapData().GetNavMeshQuery();
float const baseAngle = object->GetAngle(bot);
// 8 angles around the target starting at the bot's preferred follow
// angle (group-aware spread). For each angle, ask the engine for a
// valid nearby ground point at the requested distance — that snaps
// to terrain/collision. LOS check ignores M2 models so long-distance
// NPCs through forested terrain still pass; the mmap probe in
// MoveFarTo is the authoritative reachability check.
float const followAngle = GetFollowAngle();
float const searchSize = bot->GetObjectSize();
for (float step = 0.0f; step < 2.0f * static_cast<float>(M_PI);
step += static_cast<float>(M_PI) / 4.0f)
{
float const angle = baseAngle + step;
float x = object->GetPositionX() + std::cos(angle) * distance;
float y = object->GetPositionY() + std::sin(angle) * distance;
float const angle = followAngle + step;
float x = object->GetPositionX();
float y = object->GetPositionY();
float z = object->GetPositionZ();
object->GetNearPoint(bot, x, y, z, searchSize, distance, angle);
// LOS check at eye height.
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight()))
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight(),
VMAP::ModelIgnoreFlags::M2))
continue;
// Strict navmesh-snap validation (cmangos ClosestCorrectPoint port).
if (navMeshQuery)
{
dtQueryFilter filter;
filter.setIncludeFlags(NAV_GROUND);
filter.setExcludeFlags(NAV_GROUND_STEEP | NAV_WATER | NAV_MAGMA | NAV_SLIME);
float const point[VERTEX_SIZE] = { y, z, x };
float const extents[VERTEX_SIZE] = { 5.0f, 5.0f, 5.0f };
float closest[VERTEX_SIZE] = { 0.0f, 0.0f, 0.0f };
dtPolyRef polyRef = INVALID_POLYREF;
if (!dtStatusSucceed(navMeshQuery->findNearestPoly(
point, extents, &filter, &polyRef, closest)) ||
polyRef == INVALID_POLYREF)
continue;
float const snappedZ = closest[1];
if (std::fabs(snappedZ - z) > 10.0f)
continue;
x = closest[2];
y = closest[0];
z = snappedZ;
}
return MoveFarTo(WorldPosition(object->GetMapId(), x, y, z));
}
@ -600,50 +471,15 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject* center)
{
if (IsWaitingForLastMove(priority))
return false;
float const distance = (0.4f + rand_norm() * 0.6f) * moveStep;
float const angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float const dx = bot->GetPositionX() + distance * cos(angle);
float const dy = bot->GetPositionY() + distance * sin(angle);
float const dz = bot->GetPositionZ();
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
// Retry random samples so one bad roll doesn't lock the bot in place.
for (int attempt = 0; attempt < 8; ++attempt)
{
float distance = (0.4f + rand_norm() * 0.6f) * moveStep;
float angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);
float dz = z;
PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK, /*forceDestination=*/false);
if (!path.reachable)
continue;
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
continue;
if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight()))
continue;
// Reject samples whose straight-line passes through visual
// obstacles (trees, models) that aren't in the navmesh. The
// smooth-path step can otherwise interpolate a waypoint inside
// a tree, making the bot visibly walk through it.
if (!bot->IsWithinLOS(dx, dy, dz))
continue;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
if (moved)
{
EmitDebugMove("MoveRandomNear", "mmap", dx, dy, dz);
return true;
}
}
EmitDebugMove("MoveRandomNear", "all-fail", x, y, z);
return false;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
EmitDebugMove("MoveRandomNear", moved ? "mmap" : "fail", dx, dy, dz);
return moved;
}
bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
@ -1584,6 +1420,79 @@ bool NewRpgBaseAction::HasNearbyQuestMob(float range)
return false;
}
bool NewRpgBaseAction::HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx)
{
if (!questId)
return false;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
return false;
// Turn-in path: completed quest has no remaining mob objective.
if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
return false;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
uint32 neededCreatureEntry = 0;
uint32 neededItemId = 0;
if (objectiveIdx >= 0 && objectiveIdx < QUEST_OBJECTIVES_COUNT)
{
int32 entry = quest->RequiredNpcOrGo[objectiveIdx];
if (entry > 0 &&
qs.CreatureOrGOCount[objectiveIdx] < quest->RequiredNpcOrGoCount[objectiveIdx])
{
neededCreatureEntry = uint32(entry);
}
// Item objective sometimes lives in the same slot range.
if (objectiveIdx < QUEST_ITEM_OBJECTIVES_COUNT &&
quest->RequiredItemId[objectiveIdx] &&
qs.ItemCount[objectiveIdx] < quest->RequiredItemCount[objectiveIdx])
{
neededItemId = quest->RequiredItemId[objectiveIdx];
}
}
else if (objectiveIdx >= QUEST_OBJECTIVES_COUNT &&
objectiveIdx < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
int32 itemSlot = objectiveIdx - QUEST_OBJECTIVES_COUNT;
if (quest->RequiredItemId[itemSlot] &&
qs.ItemCount[itemSlot] < quest->RequiredItemCount[itemSlot])
{
neededItemId = quest->RequiredItemId[itemSlot];
}
}
if (!neededCreatureEntry && !neededItemId)
return false;
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!c || !c->IsInWorld() || !c->IsAlive())
continue;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
continue;
if (bot->GetDistance(c) > range)
continue;
if (neededCreatureEntry && c->GetEntry() == neededCreatureEntry)
return true;
if (neededItemId)
{
CreatureTemplate const* tmpl = c->GetCreatureTemplate();
if (tmpl && tmpl->lootid &&
LootTemplates_Creature.HaveQuestLootForPlayer(tmpl->lootid, bot))
return true;
}
}
return false;
}
ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit)
{

View File

@ -68,6 +68,12 @@ protected:
// 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:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);

View File

@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
botAI->rpgInfo.ChangeToIdle();
return false;
}
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive())
if (!bot->IsOutdoorPvPActive())
return false;
uint32 zoneId = bot->GetZoneId();
@ -113,9 +113,6 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
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
if (urand(0, 2) == 0)
return ForceToWait(urand(3000, 6000));

View File

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

View File

@ -5986,29 +5986,6 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot)
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)
{
int count = 1; // yourself

View File

@ -494,7 +494,6 @@ public:
void ImbueItem(Item* item, Unit* target);
void ImbueItem(Item* item);
void EnchantItemT(uint32 spellid, uint8 slot);
uint32 GetBuffedCount(Player* player, std::string const spellname);
int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance);
virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr);

View File

@ -720,9 +720,10 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
PathGenerator path(pathUnit);
// Source is a temp Creature, so CreateFilter's bot block doesn't
// fire — apply the same bot rules here so generated paths match
// what bots can actually walk at runtime.
path.SetExcludeFlags(NAV_GROUND_STEEP);
// 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);
@ -857,8 +858,8 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
PathGenerator path(pathUnit);
// Same reason as getPathStepFrom: temp-Creature source doesn't trip
// CreateFilter's bot block, so apply the bot rules manually.
path.SetExcludeFlags(NAV_GROUND_STEEP);
// 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

View File

@ -161,18 +161,8 @@ float TravelNodePath::getCost(Player* bot, uint32 cGold)
if (factionAnnoyance > 0)
modifier += 0.3 * factionAnnoyance; // For each level the whole path takes 10% longer.
}
if (getPathType() == TravelNodePathType::flyingMount)
{
if (!bot->IsAlive() || bot->GetLevel() < 70 || !bot->CanFly())
return -1.0f;
float flySpeed = bot->GetSpeed(MOVE_FLIGHT);
if (flySpeed < 1.0f)
flySpeed = 20.0f; // 280% base flying speed fallback
return (distance / flySpeed) * modifier;
}
}
else if (getPathType() == TravelNodePathType::flightPath || getPathType() == TravelNodePathType::flyingMount)
else if (getPathType() == TravelNodePathType::flightPath)
return -1.0f;
if (getPathType() != TravelNodePathType::walk)
@ -885,33 +875,28 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
continue;
}
if (nodePath->getPathType() == TravelNodePathType::portal ||
nodePath->getPathType() == TravelNodePathType::staticPortal) // Teleport to next node.
if (nodePath->getPathType() == TravelNodePathType::areaTrigger)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Entry point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Exit point
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject());
}
else if (nodePath->getPathType() == TravelNodePathType::transport) // Move onto transport
else if (nodePath->getPathType() == TravelNodePathType::staticPortal)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TRANSPORT,
nodePath->getPathObject()); // Departure point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject()); // Arrival point
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject());
}
else if (nodePath->getPathType() == TravelNodePathType::flightPath) // Use the flightpath
else if (nodePath->getPathType() == TravelNodePathType::transport)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLIGHTPATH,
nodePath->getPathObject()); // Departure point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); // Arrival point
// Emit the transport's full waypoint route, not just board+exit.
// Intermediate points carry NODE_TRANSPORT type so the executor
// sees consecutive transport waypoints as one block (board at
// first, disembark at last).
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject());
}
else if (nodePath->getPathType() == TravelNodePathType::teleportSpell)
else if (nodePath->getPathType() == TravelNodePathType::flightPath)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
}
else if (nodePath->getPathType() == TravelNodePathType::flyingMount)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0);
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0);
// Full taxi waypoint route; same reasoning as transport.
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
}
else
{
@ -922,14 +907,12 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
path.pop_back();
if (path.size() > 1 && prevNode->isPortal() &&
nodePath->getPathType() != TravelNodePathType::portal &&
nodePath->getPathType() != TravelNodePathType::staticPortal) // Do not move to the area trigger if we
// don't plan to take the portal.
nodePath->getPathType() != TravelNodePathType::areaTrigger &&
nodePath->getPathType() != TravelNodePathType::staticPortal)
path.erase(path.begin());
if (path.size() > 1 && prevNode->isTransport() &&
nodePath->getPathType() !=
TravelNodePathType::transport) // Do not move to the transport if we aren't going to take it.
nodePath->getPathType() != TravelNodePathType::transport)
path.erase(path.begin());
travelPath.addPath(path, PathNodeType::NODE_PATH);
@ -1283,47 +1266,153 @@ bool TravelNodeMap::GetFullPath(TravelPlan& plan,
WorldPosition botPos, uint32 botZoneId,
WorldPosition destination, Unit* bot)
{
// Capture previous pathToStart from the about-to-be-reset plan so we
// can try cropPathTo to reuse it across the per-tick re-resolve.
std::vector<WorldPosition> prevPathToStart;
for (auto const& pt : plan.steps.GetPathRef())
{
if (pt.type == PathNodeType::NODE_PREPATH)
prevPathToStart.push_back(pt.point);
else
break; // PREPATH is always at the head
}
plan.Reset();
plan.destination = destination;
// mmap-probe first: if a 40-step probe reaches dest, skip the
// graph entirely — a direct walk beats a node hop.
// mmap-probe first: if a 40-step probe makes meaningful progress,
// prefer it over the graph. Loosened from "reaches within spellDistance"
// because the strict gate falls through to graph routing whenever the
// probe stops a few yards short of the destination (e.g., bot can't
// reach the exact GO position, or destination is inside an area the
// probe can't fully enter). Graph paths come from DB-cached walk
// edges baked at offline generation time and can route through
// terrain that current mmaps treat as unwalkable.
//
// Accept the probe if EITHER:
// (a) it reaches within 30y of destination, OR
// (b) it makes >50% progress and got at least 30y total
if (botPos.GetMapId() == destination.GetMapId())
{
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
if (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
if (probe.size() >= 2)
{
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
for (size_t i = 1; i < probe.size(); ++i)
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
return true;
float const totalDist = botPos.distance(destination);
float const probeEndToDest = destination.distance(probe.back());
float const probeProgress = totalDist - probeEndToDest;
bool const closeEnough = probeEndToDest < 30.0f;
bool const meaningfulProgress = probeProgress > totalDist * 0.5f && probeProgress > 30.0f;
if (closeEnough || meaningfulProgress)
{
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
for (size_t i = 1; i < probe.size(); ++i)
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
return true;
}
}
}
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
// Find nearest nodes (zone-indexed, fast)
TravelNode* startNode = GetNearestNodeInZone(botPos, botZoneId);
if (!startNode)
startNode = GetNearestNodeOnMap(botPos);
// K-nearest start + end node candidates (cmangos parity: K=5).
// Iterate combinations — first pair with a graph route wins. The
// single-nearest may have no route while the 2nd/3rd does.
constexpr uint32 K = 5;
auto pickKNearest = [&](WorldPosition pos, uint32 zoneId) -> std::vector<TravelNode*>
{
std::vector<TravelNode*> const& zoneNodes = GetNodesInZone(zoneId);
std::vector<TravelNode*> candidates(zoneNodes.begin(), zoneNodes.end());
if (candidates.empty())
{
// Fallback to per-map scan
for (TravelNode* n : nodes)
if (n && n->getPosition()->GetMapId() == pos.GetMapId())
candidates.push_back(n);
}
if (candidates.empty())
return {};
uint32 n = std::min<uint32>(K, candidates.size());
std::partial_sort(candidates.begin(), candidates.begin() + n, candidates.end(),
[pos](TravelNode* i, TravelNode* j) { return i->fDist(pos) < j->fDist(pos); });
candidates.resize(n);
return candidates;
};
uint32 destZone = sMapMgr->GetZoneId(PHASEMASK_NORMAL, destination);
TravelNode* endNode = GetNearestNodeInZone(destination, destZone);
if (!endNode)
endNode = GetNearestNodeOnMap(destination);
std::vector<TravelNode*> startCandidates = pickKNearest(botPos, botZoneId);
std::vector<TravelNode*> endCandidates = pickKNearest(destination, destZone);
if (!startNode || !endNode || startNode == endNode)
if (startCandidates.empty() || endCandidates.empty())
return false;
if (!startNode->hasRouteTo(endNode))
TravelNode* startNode = nullptr;
TravelNode* endNode = nullptr;
TravelNodeRoute route;
for (TravelNode* s : startCandidates)
{
for (TravelNode* e : endCandidates)
{
if (!s || !e || s == e)
continue;
if (!s->hasRouteTo(e))
continue;
TravelNodeRoute r = GetNodeRoute(s, e, nullptr);
if (r.isEmpty())
continue;
startNode = s;
endNode = e;
route = r;
break;
}
if (!route.isEmpty())
break;
}
if (route.isEmpty() || !startNode || !endNode)
return false;
TravelNodeRoute route = GetNodeRoute(startNode, endNode, nullptr);
if (route.isEmpty())
return false;
WorldPosition startNodePos = *startNode->getPosition();
WorldPosition endNodePos = *endNode->getPosition();
// pathToStart: mmap-path from bot to the first node. Try cropping
// the previous pathToStart first (cmangos parity) — if it still
// reaches the chosen startNode within reactDistance we avoid a full
// re-probe. Falls back to fresh getPathTo if crop fails or invalid.
std::vector<WorldPosition> pathToStart;
if (!prevPathToStart.empty())
{
std::vector<WorldPosition> cropped = prevPathToStart;
bool ok = startNodePos.cropPathTo(cropped, sPlayerbotAIConfig.reactDistance);
if (ok && cropped.size() >= 2)
pathToStart = cropped;
}
if (pathToStart.empty() && bot && botPos.GetMapId() == startNodePos.GetMapId())
{
std::vector<WorldPosition> probe = botPos.getPathTo(startNodePos, bot);
if (probe.size() >= 2)
pathToStart = probe;
}
if (pathToStart.empty())
pathToStart = {botPos};
// pathToEnd: mmap-path from the last node to the destination.
// Single-map case: use bot's PathGenerator directly.
// Cross-map case: pass nullptr — getPathTo constructs a tempCreature
// on the destination's base map so we can pathfind there even though
// bot isn't loaded into it.
std::vector<WorldPosition> pathToEnd;
if (endNodePos.GetMapId() == destination.GetMapId())
{
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
std::vector<WorldPosition> probe = endNodePos.getPathTo(destination, pathBot);
if (probe.size() >= 2)
pathToEnd = probe;
}
if (pathToEnd.empty())
pathToEnd = {destination};
std::vector<WorldPosition> pathToStart = {botPos};
std::vector<WorldPosition> pathToEnd = {destination};
plan.steps = route.BuildPath(pathToStart, pathToEnd, nullptr);
return !plan.steps.empty();
@ -1507,11 +1596,9 @@ void TravelNodeMap::generateStartNodes()
void TravelNodeMap::generateAreaTriggerNodes()
{
// Entrance nodes
for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports())
{
AreaTriggerTeleport const& atEntry = itr.second;
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first);
if (!at)
continue;
@ -1521,7 +1608,6 @@ void TravelNodeMap::generateAreaTriggerNodes()
atEntry.target_Orientation);
std::string nodeName;
if (!outPos.isOverworld())
nodeName = outPos.getAreaName(false) + " entrance";
else if (!inPos.isOverworld())
@ -1532,12 +1618,10 @@ void TravelNodeMap::generateAreaTriggerNodes()
TravelNodeMap::instance().addNode(inPos, nodeName, true, true);
}
// Exit nodes
// Exit nodes + area-trigger link
for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports())
{
AreaTriggerTeleport const& atEntry = itr.second;
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first);
if (!at)
continue;
@ -1547,7 +1631,6 @@ void TravelNodeMap::generateAreaTriggerNodes()
atEntry.target_Orientation);
std::string nodeName;
if (!outPos.isOverworld())
nodeName = outPos.getAreaName(false) + " entrance";
else if (!inPos.isOverworld())
@ -1555,16 +1638,12 @@ void TravelNodeMap::generateAreaTriggerNodes()
else
nodeName = inPos.getAreaName(false) + " portal";
//TravelNode* entryNode = TravelNodeMap::instance().getNode(outPos, nullptr, 20.0f); // Entry side, portal exit. //not used, line marked for removal.
TravelNode* outNode = TravelNodeMap::instance().addNode(outPos, nodeName, true, true);
TravelNode* inNode = TravelNodeMap::instance().getNode(inPos, nullptr, 5.0f);
TravelNode* outNode = TravelNodeMap::instance().addNode(outPos, nodeName, true, true); // Exit size, portal exit.
TravelNode* inNode = TravelNodeMap::instance().getNode(inPos, nullptr, 5.0f); // Entry side, portal center.
// Portal link from area trigger to area trigger destination.
if (outNode && inNode)
{
TravelNodePath travelPath(0.1f, 3.0f, (uint8)TravelNodePathType::portal, itr.first, true);
TravelNodePath travelPath(0.1f, 3.0f, (uint8)TravelNodePathType::areaTrigger, itr.first, true);
travelPath.setPath({*inNode->getPosition(), *outNode->getPosition()});
inNode->setPathTo(outNode, travelPath);
}

View File

@ -39,12 +39,11 @@
//
// Edge types (TravelNodePathType):
// walk(1) — Walk via navmesh waypoints (stored in DB)
// portal(2) — AreaTrigger teleport (auto-discovered at startup)
// 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)
// flyingMount (7) — Use Bots Flying mount to travel (Not currently enabled)
//
// 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
@ -91,12 +90,13 @@ enum class TravelNodePathType : uint8
{
none = 0,
walk = 1,
portal = 2,
areaTrigger = 2,
transport = 3,
flightPath = 4,
teleportSpell = 5,
staticPortal = 6,
flyingMount = 7
// 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.
@ -267,10 +267,9 @@ public:
bool isPortal()
{
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 false;
}
@ -414,11 +413,12 @@ enum class PathNodeType : uint8
NODE_PREPATH = 0,
NODE_PATH = 1,
NODE_NODE = 2,
NODE_PORTAL = 3,
NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5,
NODE_TELEPORT = 6,
NODE_FLYING_MOUNT = 7
// value 6 (NODE_TELEPORT) reserved — no consumer; re-add when a
// teleport-spell handler / generator returns.
NODE_STATIC_PORTAL = 7
};
struct PathNodePoint
@ -564,9 +564,7 @@ struct TravelPlan
// Spline scratch (used by executor):
std::vector<G3D::Vector3> walkPoints;
bool splineActive{false};
uint32 splineStartTime{0};
uint32 expectedDuration{0};
uint32 expectedDuration{0}; // used to derive the lastMove delay
// Taxi scratch:
std::vector<uint32> route;
@ -579,8 +577,6 @@ struct TravelPlan
steps.clear();
stepIdx = 0;
walkPoints.clear();
splineActive = false;
splineStartTime = 0;
expectedDuration = 0;
route.clear();
}

View File

@ -83,8 +83,6 @@ bool PlayerbotAIConfig::Initialize()
sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000);
returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000);
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);
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
@ -99,7 +97,6 @@ bool PlayerbotAIConfig::Initialize()
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);
@ -116,6 +113,32 @@ bool PlayerbotAIConfig::Initialize()
highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65);
autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true);
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);
maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f);
LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"),

View File

@ -40,6 +40,13 @@ enum class HealingManaEfficiency : uint8
SUPERIOR = 32
};
enum class AutoPartyBuffMode : uint8
{
DISABLED = 0,
RAID_ONLY = 1,
GROUP_OR_RAID = 2
};
enum NewRpgStatus : int
{
//Initial Status
@ -89,11 +96,13 @@ public:
bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
targetPosRecalcDistance, farDistance, healDistance, aggroDistance;
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
uint32 lowMana, mediumMana, highMana;
bool autoSaveMana;
uint32 saveManaThreshold;
AutoPartyBuffMode autoGreaterBlessings;
AutoPartyBuffMode autoPartyBuffs;
bool autoAvoidAoe;
float maxAoeAvoidRadius;
std::set<uint32> aoeAvoidSpellWhitelist;
@ -146,12 +155,6 @@ public:
uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay;
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
bool enableFishingWithMaster;
uint32 classMatchingProfessionChance;