Compare commits

...

55 Commits

Author SHA1 Message Date
bashermens
6cf7f1aaef [CHORE] Util classes format and simple cleanup with generated resources (#2028)
- Did some basic formatting
- Some generated docs
- Cleaned header/impl Helper.css
- Moved PerfMonitor from util to bot/handler/command

Still a freaking mess though, but its a start i guess. Cant ask ppl to
add more or make use of those when its so messy.
2026-01-18 00:43:44 +01:00
bashermens
9f54d7e702 Removed the expansion folder from dungeons (#2027)
In order to make consistent with raids but also to shorten max used
length directory for windows builds
2026-01-17 21:55:08 +01:00
bashermens
aeaaee15da [FIX] Finalized structure! (do not start fixing PR merge structure conflict till this is merged) (#2025)
Finalized
2026-01-17 14:38:12 +01:00
bashermens
a1137dbddc [FIX] Folder restructure (#2018)
As requested

Poll
```
1. Yes
2. Yes
3. Maybe, but yes
```

---------

Co-authored-by: Celandriel <22352763+Celandriel@users.noreply.github.com>
2026-01-17 10:34:58 +01:00
bashermens
2eb98c3233 [BUILD] Windows shorter build path (#2021) 2026-01-16 17:39:12 +01:00
Keleborn
29613e29b7 Bug - Selfbot can trigger crash in printstats (#2013)
Adding a guard against bots that may be registered in `playerBots`
variable as selfbot have appeared to crash here.

Edit: The exact circumstances that caused the incorrect registration are
under investigation as it seems to be 'atypical' behavior.
2026-01-15 00:42:37 +01:00
privatecore
e2c203a35e Fix the duplicate spells issue during randomization (#2014)
**Original issue:**
Bots/Characters received duplicate spells during randomization process.

**Root cause:**
When `PlayerbotFactory::Randomize` is processed, `InitSkills` is called
AFTER `InitAvailableSpells`, which causes the duplicate spells issue
because the skillline ability spell is already learned when processing
spells from trainers (`InitAvailableSpells`).

We simply need to change the order of the randomization process: skills
should be handled first, then spells. An alternative approach would be
to adjust the skillline abilities and check each spell for every skill,
but that seems redundant since we already have checks for the trainer's
spells.

`InitSkills` -> `SetRandomSkill` -> `SetSkill` ->
`learnSkillRewardedSpells` -> `learnSpell` -> duplicate error...

**Steps to reproduce:**
1. create common character and login with it
2. set level for this character eq. 80 (`.set level 79`)
3. create and run macro:
```
/g .playerbots bot initself
/g .saveall
```
4. logout -> login and run macro again

**Note:**
Also added checks for the trainer's spells since `GetSpell` can return
nullptr. Updated `LearnQuestSpells` after recent changes and used the
same logic and implementation from `Player::learnQuestRewardedSpells`.

Yes, we need to cast spells, or we should handle all spell effects that
quests/trainers have (for ex.: `SPELL_EFFECT_SKILL_STEP`,
`SPELL_EFFECT_UNLEARN_SPECIALIZATION`, `SPELL_EFFECT_BIND`).
2026-01-15 00:42:19 +01:00
bashermens
1b1ed18a23 Minor fix (#2015) 2026-01-15 00:42:09 +01:00
Not
2ab73c1fd5 Fix deprecated vsprintf usage in Engine logging 2nd edition (#1978)
Replace deprecated vsprintf with vsnprintf to eliminate compiler warning
and prevent potential buffer overflow. Updated to latest commit.

Tested in game and it seemed to log actions just fine. I just basically
added a buffer size by using the current vsnprintf lib instead.
2026-01-14 18:41:56 +01:00
OlegKungurov
6b97c379ba Fix fishing AI position matching precision (#1992)
**Problem**: FishingAction::isUseful() used strict `pos ==
bot->GetPosition()` comparison causing "Rog A:go fishing - USELESS" when
bot was within 0.5-1m of fishing spot (normal positioning tolerance).

**Solution**: Replace exact coordinate match with 1.5m tolerance using
`fabs(posX - botX) < 1.5f` for all axes. Added `#include "Config.h"` for
AI_VALUE stability.

**Result**: Master fishing now reliably triggers and completes cycle:
- move near water → go fishing → use bobber ✓

Closes #fishing-useless-bug

---------

Co-authored-by: Кунгуров Олег <okungurov@rapidsoft.ru>
2026-01-14 18:41:06 +01:00
bashermens
965d300203 [HOTFIX] Revert "Fix/Feat: Stop bots in party from PVP when master isn't, and … (#2003)
…PVP probablity system (#1914)"

This reverts commit 02e8465a3b.


[worldserver_gdb_20260111_000520.log](https://github.com/user-attachments/files/24547263/worldserver_gdb_20260111_000520.log)


Noticed the server kept crashing all the sudden, first thought it was
local issue but see dump.
2026-01-11 01:52:25 +01:00
privatecore
dc55ecfd9c Fix wrong logic when processing quest reward spells during maintenance level-up action (#2001)
The previous behavior failed cuz if a spell has no
`SPELL_EFFECT_LEARN_SPELL`, bots learned the original spell from
`quest_template` rewards w/o any checks, when they should never learn
any spells specified in the `RewardSpell` and/or `RewardDisplaySpell`.

PR: https://github.com/mod-playerbots/mod-playerbots/pull/1996 Thx
@Wishmaster117 for the collaboration!

Fix issue https://github.com/mod-playerbots/mod-playerbots/issues/1759
2026-01-10 01:31:54 +01:00
Crow
59d6eb139e Exclude additional test enchants from bots (#1952)
Added two test enchants to exclude from the list of enchants that bots
can apply: 19927 and 44119. I also reordered the list to be in
alphanumeric order.

The important exclusion to add is 44119:
https://www.wowhead.com/wotlk/spell=44119/enchant-bracer-template

It is a test enchant that gives 34 AP on bracers; this is stronger than
any other bracer enchant for level 70 physical attackers so bots are
applying it.
2026-01-09 22:16:33 +01:00
Crow
00171a8c82 Minor fixes to .dist descriptions (#1945)
Clarified that MinEnchantingBotLevel also determines whether maintenance
will socket gems, in addition to applying enchants.

Fixed max iLevel for ZA gear in RandomGearScoreLimit description (iLevel
138 is the max that drops from the final boss, but the 3rd timed chest
gives iLevel 141 equipment).
2026-01-09 19:57:29 +01:00
NoxMax
02e8465a3b Fix/Feat: Stop bots in party from PVP when master isn't, and PVP probablity system (#1914)
There are two related PVP components in this PR. First is the simple yet
fundamental change to bot behaviour when they are in party. Right now
bots with a master will go into PVP when there's a nearby PVP target,
even if master is not in PVP. This absolutely should not happen. Bots
should not consider PVP at all if master is not in PVP. The fix is only
3 lines in EnemyPlayerValue

The second component is introducing PVP probabilities, to make decisions
more realistic. Right now even a level 1 bot will 100% go into PVP if it
sees a level 80 PVP target. They can't help themselves. So the change
here addresses that insanity. Several thresholds (subject to community
review) are introduced:

1. Bots will not fight a target 5 or more levels higher than them
2. Bots have a 25% chance starting a fight with a target +/- 4 levels
from them.
3. Bots have a 50% chance starting a fight with a target +/- 3 levels
from them.
4. Bots have a 75% chance starting a fight with a target +/- 2 levels
from them.
5. Bots have a 100% chance starting a fight with a target +/- 1 level
from them.
6. Bots have a 25% chance starting a fight with a target 5 or more
levels below them (ganking. thought it would be funny, and technically
realistic of player behaviour)

Exception of course exist for BG/Arena/Duel, and in capitals where bots
will always PVP. Also bots will always defend themselves if attacked.

Few notes: 
1. The if/ else if logic can be further simplified, but only if we use
thresholds that are different by one. So current logic allows for
flexibility of using values like 10/7/5/3 instead of 5/4/3/2.
2. The caching system is per-bot basis. So for some target X, if some
bot decides to attack it, another bot will make its own decision. At
first I used a simplified global system (thinking there might be
performance concerns) where if one bot decides to attack a target then
they all do, but when I switched to the more realistic per-bot basis, I
didn't see an effect on performance.
3. Variables are obviously not configurable right now. I'm starting to
see Bash's POV that maybe we have too many configs 😬 Still,
they can be easily exposed in the future, and if someone is reading this
then, remember to change constexpr to const.
2026-01-08 22:44:09 +01:00
Crow
f53a8704eb Remove InitSpells() (#1983)
InitSpells() is never called, and even if it were to be, it would do
nothing but call InitAvailableSpells().
2026-01-08 11:46:36 -08:00
bashermens
e5525958c8 [BUG FIX] Movement flag update called for bots while rooted (#1991)
Fix for the error "Attempted sending heartbeat with root flag for guid"

The core does not allow movement flag updates when unit/player has the
MOVEMENTFLAG_ROOT flag. Change scope bots alone.
2026-01-08 20:38:49 +01:00
Alex Dcnh
9ae457d069 [BUG FIX] - Preserve buffs on summon by avoiding aura clears (#1958)
### Summary

- Add a preserveAuras flag to summon teleport helpers.
- Keep auras when using the summon command.
- Leave meeting stone behavior unchanged (auras can still be cleared
there).

### Motivation
Summon command was clearing buffs (e.g., Hellscream’s Warsong in ICC)
because auras were explicitly interrupted before teleporting. This
change keeps existing meeting stone behavior but preserves auras for the
summon path to avoid losing valid buffs.

### Details
Solve : 
https://github.com/mod-playerbots/mod-playerbots/issues/1862
https://github.com/mod-playerbots/mod-playerbots/issues/1942

### Testing
Go to ICC and téléport Bots
2026-01-07 15:24:41 +01:00
privatecore
c9e98a6b4e [BUG FIX] the issue where bots on vehicles cant move (#1969)
Fix the [issue
#1964](https://github.com/mod-playerbots/mod-playerbots/issues/1964)
where bots on vehicles cant move. Transfer the allowed movement logic to
`PlayerbotAI::CanMove` -- Ulduar raid strategy also used this method for
the Malady of the Mind trigger.

The only remaining `IsRooted` check is in
`MovementAction::UpdateMovementState`. I'm not sure if this also needs
to be updated for the vehicles case, but on paper, everything should
work as intended. A more complex solution to fix this issue can be found
in the
[comment](https://github.com/mod-playerbots/mod-playerbots/issues/1902#issuecomment-3703795779),
but for now, these changes are safer.
2026-01-07 15:24:18 +01:00
bashermens
3d9623f119 [CRASH FIX] Various crash fixes in regard to defensive checks around unit/targets and finer grained checks on tele ack (#1951)
Some basic defense checks on mainly unit/targets used in public
functions, and some minor tweaks with teleport ack.
https://github.com/mod-playerbots/mod-playerbots/issues/1934
https://github.com/mod-playerbots/mod-playerbots/issues/1957
2026-01-07 15:22:36 +01:00
Nicolas Lebacq
c9cc4324d3 NextAction refactoring to eliminate sentinel arrays and pointers (#1923)
<!--
> [!WARNING]
> **This is a DRAFT PR.**
> The structure is not definitive. The code might not be optimised yet.
It might not even start nor compile yet.
> **Don't panic.  It's going to be ok. 👌 We can make modifications, we
can fix things.** 😁
-->
# Description

This PR aims to refactor the NextAction declaration to achieve two
goals:

## Eliminate C-style sentinel arrays

Currently, a double pointer (`NextAction**`) approach is being used.
This an old pre-C++11 (< 2011) trick before `std::vector<>` became a
thing.
This approach is painful for developers because they constantly need to
declare their `NextAction` arrays as:
```cpp
NextAction::array(0, new NextAction("foo", 1.0f), nullptr)
```
Instead of:
```cpp
{ new NextAction("foo", 1.0f) }
```
The first argument of `NextAction::array` is actually a hack. It is used
to have a named argument so `va_args` can find the remaining arguments.
It is set to 0 everywhere but in fact does nothing. This is very
confusing to people unfamiliar with this antiquated syntax.
The last argument `nullptr` is what we call a sentinel. It's a `nullptr`
because `va_args` is looking for a `nullptr` to stop iterating. It's
also a hack and also leads to confusion.

## Eliminate unnecessary pointers for `NextAction`

Pointers can be used for several reasons, to cite a few:
- Indicate strong, absolute identity.
- Provide strong but transferable ownership (unlike references).
- When a null value is acceptable (`nullptr`).
- When copy is expensive.

`NextAction` meets none of these criteria:
- It has no identity because it is purely behavioural.
- It is never owned by anything as it is passed around and never fetched
from a registry.
- The only situations where it can be `nullptr` are errors that should
in fact throw an `std::invalid_argument` instead.
- They are extremely small objects that embark a single `std::string`
and a single `float`.

Pointers should be avoided when not strictly necessary because they can
quickly lead to undefined behaviour due to unhandled `nullptr`
situations. They also make the syntax heavier due to the necessity to
constantly check for `nullptr`. Finally, they aren't even good for
performance in that situation because shifting a pointer so many times
is likely more expensive than copying such a trivial object.

# End goal

The end goal is to declare `NextAction` arrays this way:
```cpp
{ NextAction("foo", 1.0f) }
```

> [!NOTE]
> Additional note: `NextAction` is nothing but a hacky proxy to an
`Action` constructor. This should eventually be reworked to use handles
instead of strings. This would make copying `NextAction` even cheaper
and remove the need for the extremely heavy stringly typed current
approach. Stringly typed entities are a known anti-pattern so we need to
move on from those.
2026-01-06 12:37:39 +01:00
Keleborn
b13fb7d12a Bugfix - PlayerbotGuildMgr not respecting real player guilds. (#1972)
Minor fix to address small bug introduced by #1913
2026-01-06 01:31:02 +01:00
Revision
962fdeb3d1 Updated to support latest master (#1965)
This needs extensive testing.

What's important is spells given to bots. Class spells, mounts,
professions etc. Make sure they get the spells they should, when they
should.

Requires https://github.com/mod-playerbots/azerothcore-wotlk/pull/132
2026-01-05 15:06:45 +01:00
Keleborn
83c6977de5 Refactor guild managment into a singleton (#1913)
The idea is to centralize the creation, assignment, and management of
bot guilds into a single class that can be referenced.

The way this is intended to work.

when the manager is created, if the config option to delete guilds is
set, then it deletes all bot guilds.

On startup 
1. Load all guild names from database. Shuffle keys for some
randomization.
2. Load Guilds from database
3. For existing guilds, identify the guild faction, number of members,
and assess if the guild is 'full' based on the number of bots set in
config.
4. Determine if the leader of the guild is a real player based on the
leader account.
5. Mark any playerbot guild names as not available (false).

The validation process (2-5) is set to run once an hour. 

Guild Creation.
Now guild creation occurs on an as needed bases during the
initialization process. Previously, all of the guilds would be created
at once, and then randomly assigned.
When a bot is not in a guild during initialization, it will check if
there are any partially filled guilds of that bots faction where the bot
can be assigned to. If not, and the cache of bot guilds is less than the
set number in config, it will randomly return the available name. This
then goes to the CreateGuild function where the core guild manager
creates a guild, the guild emblem is set, and the cache updated.
If a bot is assigned to guild, but fails to join then it throws an
error.

Checking for real player guilds function now lives in the guild manager.

---------

Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2026-01-03 15:15:28 +01:00
Revision
686fe513b2 Removed strategies for Naxxramas until they can be rewritten without core changes (#1961)
Merging this isn't required but rather to provide an option to core
changes.

The scripts can be written from scratch when someone feels like doing it
without touching the core.

This shouldn't be merged unless
https://github.com/mod-playerbots/azerothcore-wotlk/pull/130 is merged
too.
2026-01-03 14:50:35 +01:00
Revision
61402e83a1 Fixed filename (#1963)
Added missing underscores to the filename
2026-01-03 14:47:31 +01:00
Crow
b16789fa54 Remove "potion" strategy from arenas + code cleanups (#1922)
Lines 481 through 484 are the only substantive changes. Bots were having
the potion strategy added in arenas, and since potions are not allowed
in arenas, this caused them to lock up and attempt to repeatedly drink
potions when under the applicable health or mana trigger thresholds. Now
they won't have the potion strategy in arenas.

Otherwise, I noticed a bunch of magic numbers for spec tabs and so went
ahead and did some refactoring:
1. All references to spec tab numbers now use the appropriate constant
name.
2. A bunch of extra braces were deleted.
3. DEATHKNIGHT_TAB was changed to DEATH_KNIGHT_TAB, and
HUNTER_TAB_BEASTMASTERY was changed to HUNTER_TAB_BEAST_MASTERY, to
reflect the correct names of the class and spec.
4. Deleted some comments that were clearly unneeded. There's much more
that can be cleaned up, and probably the entire logic sequence for
adding/removing strategies in AiFactory.cpp can be redone, but that's
above my pay grade.
2025-12-30 21:17:14 +01:00
BeardBear
8f638b6a66 Add secure login handling for playerbots (#1953)
Introduces PlayerbotsSecureLoginServerScript to handle secure login
scenarios for playerbots. Ensures that if a playerbot is already online
when a login is attempted, it is properly logged out before allowing the
new session. Registers the new script in the Playerbots initialization.
2025-12-30 21:13:36 +01:00
Keleborn
33f5e733dc feat. Enable bots to respond to GM messages. (#1909)
I found an existing config option that allows bots to speak without a
master, and extended its use so that protected commands (nc, co) are
taken from a bot by GM players. This is primarily a debugging tool and
shouldnt be on generally.

My testing of this is the following. 
With the option off, no error is seen, but no text output is shown. 

With the option on 
GM accounts (Whether GM is on or off) will respond to nc and co commands
by just saying the output directly. Strategies can be changed by GMs for
bots whom they are not masters for (Note, im pretty sure that a GM could
change the strategies even before this change, you just couldnt see the
output of that.)

For non GM characters you get "Invite me to your group first" with the
option on/off.
2025-12-30 00:53:37 +01:00
Keleborn
9917863ca1 Feat. Add Fishing action and fish with master. (#1433)
### Update :Thank you to @notOrrytrout from prompting me to work on
this. Its been a huge learning experience.

With @notOrrytrout I started working on enabling bot fishing with
master, but also on their own.
The first commit didnt crash, showing that it was possible to have a bot
cast when master does. Currently it compiles but crashes when you try to
fish with a bot in the group, whether the bot has fishing or not. It
makes me think that the check in FishingValues is broken somehow, but I
cant figure out how.

---------

Co-authored-by: bash <31279994+hermensbas@users.noreply.github.com>
2025-12-27 19:50:18 +01:00
bashermens
2317652d72 Full revert: no xp bot when master has xp disabled (#1948)
Will reimplement the feature later in time.
2025-12-25 03:00:21 +01:00
bashermens
1fcd6c5cda Quickfix: performance problems (XP gain when master has XP turned off) (#1947)
Disable bot XP gain when master has XP turned off feature has a big
performance impact.

Instead of reverting placed back to original code before the feature
PR's for now as quickfix
2025-12-25 02:07:59 +01:00
privatecore
88016789ba Quick fix for CMSG_FORCE_MOVE_ROOT_ACK and CMSG_FORCE_MOVE_UNROOT_ACK (#1937)
**Original issue:**
https://github.com/mod-playerbots/mod-playerbots/issues/1902
**Original cause:** charmed bot (with lost client control) got rooted at
the same time.

**How to reproduce:**
1. Spawn creatures 11350 (x3) and 11830 (x3) using the command: `.npc
add <entry>` in a quiet place.
2. Create any party with random bots or alt bots (should be 60-65
levels), make sure there is at least one healer.
3. Set the healer's mana to a high value, like 100M, using command:
`.mod mana <value>`.
4. Start the fight while continuously respawning creatures with: `.resp
all`.
5. When console starts to spam 'heartbeat' messages, check your party
members' movement flags to identify which one has `MOVEMENTFLAG_ROOT`
0x00000800 (2048) using the command: `.debug move`.

This PR will not fix ALL 'heartbeat' issues, as
`ServerFacade::SetFacingTo` still sends `SendMovementFlagUpdate` while
bots can have the `MOVEMENTFLAG_ROOT` flag.
2025-12-25 00:01:42 +01:00
bashermens
6be860c967 [Stability] Various crash fixes based on Regrad fixes and crashlogs. (#1928)
These contains various fixes, fixes that have history worked one in past
more then once as person as group, aswell @Wishmaster117. But due
various reasons we had to drop them due priority or simply timewise.
These fixes have recollected again by @Regrad based on his crash logs.

Most crash logs we have, i am talking 30+ of them, to many to post here.
@Regrad running a larger server 100+ real players with bots, which means
he will walk into issues that most of us wont or are extremely difficult
to reproduce.

@Regrad used LLM to solve them based on crash log and mentioned his
server crashes almost disappeared, instead of redoing every single PR
and pull them apart. I tried to keep his bunch of changes together as
whole, reviewed them, some redone, verified again etc etc. This is not
how would normally do this. But since i want @Regrad being able to
confirm, we need this in a package as a whole. Pulling them apart in the
current situation is simply to much, to complicated in the verification
process.

So this PR is open and in my opinion has priority above others, but
@Regrad is only person who can give the green light for the
mod-playerbot changes for now.

I, we spend huge amount of time into these issues over last couple of
months. I will put other PR's on hold for abit.

---------

Signed-off-by: Engardium <regradius@gmail.com>
Co-authored-by: Engardium <regradius@gmail.com>
2025-12-24 13:24:29 +01:00
Alex Dcnh
9971622093 Core - Fixe raid markers persists after target dead causes issue with bots running off (#1845)
This PR fixes #1833 where bots could keep chasing a raid target icon
(usually skull) across very large distances or even different maps after
the mark was set. It replaces the previous attempt with a simpler design
that keeps values generic and moves context logic into triggers/actions.

Changes

- RtiTargetValue now ignores RTI targets that are farther than
sPlayerbotAIConfig->sightDistance from the bot or the master (same map
only), while still using AttackersValue::IsValidTarget and LOS checks.
- AttackersValue is reverted to its original behavior (no special RTI +
IsInCombat logic).
- RTI triggers only react to "rti target" in combat, as suggested in
review (“that condition should be check in trigger…”).
- AttackRtiTargetAction keeps a small local fallback: if "rti target" is
null, it resolves the raid icon directly from the group so chat commands
like “attack rti target” still work, including out of combat.

Behavior

- Bots no longer run across the world to chase a stale skull far away
from the group.
- When the group comes back near the marked mob (within sight distance),
the skull is again used as a normal focus hint.
- Automatic RTI behavior is limited to combat via triggers, while
explicit chat commands still work out of combat.

Testing

- Marked a mob with skull, killed it, moved far away / changed zone,
then let it respawn: bots did not chase it from afar.
- In a normal dungeon/raid pack, bots still focus skull in combat.
- Passing near a previously marked mob: bots only react once it is back
within sight distance.

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2025-12-23 20:42:29 +01:00
Crow
895df9b197 Magtheridon timer keys fix (#1936)
Same deal as Karazhan--changing map id to instance id for timer keys to
prevent conflicts from multiple groups (plus other tweaks with respect
to the timers). This strategy could use a broader refactor, but that's
for another day--this PR is just to address the timer logic.
2025-12-23 09:04:36 +01:00
Crow
467b63b840 Fix Karazhan timers (#1926)
I'm not 100% sure but think that the current implementation of timers
for Attumen, Netherspite, and Nightbane with the instance's map id as
the key would result in the timers being shared across separate raids
running the instance at the same time. I've refactored to use the
instance id as the key for all shared timers so to the extent this was
an issue, it shouldn't be anymore.

I also made some minor tweaks to the tank positioning for BBW and
Curator as I noticed there was some weirdness in the logic that I
neglected to address with the refactor.
2025-12-21 21:40:19 +01:00
HennyWilly
66f5f597bb Re-enable no-xp-feature with better checks (#1935)
The feature `Disable bot XP gain when master has XP turned off` (#1910)
was unintentionally disabled in #1929.
This PR re-enables it with better checks in order to prevent crashes
when playing together with the individual progression module.
2025-12-21 21:28:39 +01:00
privatecore
cafbd4681e Hotfix after recent changes related to the refactoring of IsMovingAllowed (#1933)
Hotfix for the issue where bots can't enter/move on transports
(elevators, zeppelins, ships, etc.). Change the logic to determine if
bot is on a vehicle. According to the current implementation in
AzerothCore, `GetVehicle()` is the most common approach for this.
Additionally, other checks related to the vehicle: `GetBase`, `IsAlive`,
`GetVehicleInfo`, and `GetSeatForPassenger` -- are processed inside
`IsInVehicle`.

This should be more than enough to determine if the bot is on a vehicle
and can/cant control it.

Issue: https://github.com/mod-playerbots/mod-playerbots/issues/1927
2025-12-20 23:51:32 +01:00
bashermens
f5c84ee7ff server_crash_fix: OnPlayerGiveXP (#1929)
[fef0ed4072c8_worldserver.exe_.18-12_23-30-44.txt](https://github.com/user-attachments/files/24258638/fef0ed4072c8_worldserver.exe_.18-12_23-30-44.txt)

- Just added common logic and defense coding
- Optimized isRandombot while at it

Just for clarification i am aware the trigger of bug lies with the
progression mod. Regardless it should not happen.
2025-12-19 23:08:18 +01:00
Keleborn
b6f882886d FixListSpellsWithCache (#1931)
This is based off of Wishmasters rewrite of spell PR. #1912 and #1843,
and partial #1918
I created a new cache singleton with the required code to reference as
needed.
I clarified some variable names for additional clarity in how they
functioned.

This requires a wiki update to better describe the functionality that
was already defined in code.

Commands
Spells - Returns all spells
Spells <Profession> Returns only the spells in that profession
+<profession> Returns only the recipies that the bot has materials to
craft
<profession> `x - y` Craftable items within those levels
<profession> <slot> e.g. Chest, returns craftable items within that
slot.

Its messy whether what combinations work for commands, but fixing that
will come when bot professions are enabled.

Edit:
To test you can teach a bot various professions by going to a trainer
with them.
Using gm command .setskill you increase their skill level and with
maintenance teach the commands.

From wishmasters PR he detailed various commands to test

spells
spells first aid
spells tailoring
spells 20-40
spells +tailoring
spells head

---------

Co-authored-by: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com>
2025-12-19 23:08:01 +01:00
HennyWilly
c1222da8b0 Disable bot XP gain when master has XP turned off (#1910)
Addresses #1846

This PR disables XP gain for bots whose non-bot master has disabled XP
gain via an "Experience Eliminator" NPC.

If the current master has disabled XP gain, randombots in the group and
addclass-bots belonging to the player won't gain XP.
Randombots not grouped with a player will continue to gain XP as normal.

Discussed points:
- `Should this feature only be enabled via a new configuration value?`:
Comments currently tend towards no config value.

Open points:
- Should bots be allowed to gain XP until they reach the player's level,
even if the player has disabled XP gain?
2025-12-16 22:23:34 +01:00
NoxMax
00cb177c86 Fix: Allow bots to duel in PVP prohibited areas (#1906)
Noticed that if you ask a bot to duel in a PVP prohibited area, it will
accept, and do nothing. I thought about making the bot reject the
request, but if you (the real player) want to duel with it, the duel
should happen. This is just a minor fix to allow bots to duel if you ask
them to in such areas.

Tested with bots in party, random bots of the same faction, and random
bots of the opposite faction. All behaved the same before and after fix.
An example place to test is Zim'Torga in Zul'Drak which is by default is
a PVP prohibited area.

- Before fix, you challenge a bot, they accept and turn red, then they
either just stay where they are or wander off.

- After fix, bot attacks you within the PVP prohibited area when the
duel starts.
2025-12-16 18:38:50 +01:00
privatecore
5f697e806e Rerwite is moving allowed logic + fix root flag heartbeat spam (#1908)
Okay, what have been done:

1. Fix heartbeat spam for root flag: check against `MOVEMENTFLAG_ROOT`
flag (`IsRooted`) instead of `HasRootAura`.
2. Rewrite `IsMovingAllowed` - place checks from most common to the
rarest.
3. Remove unnecessary checks: `HasRootAura`, `HasConfuseAura`,
`HasStunAura` - handled by AuraEffects and set unit state flags
`UNIT_STATE_ROOT`, `UNIT_STATE_CONFUSED`, `UNIT_STATE_STUNNED` -
`UNIT_STATE_LOST_CONTROL` already handles confused and stunned (rooted
checked with `IsRooted` method).
4. Combine traveling state checks for taxi flights:
`UNIT_STATE_IN_FLIGHT` + MM flag `FLIGHT_MOTION_TYPE`.
5. Simplify check against being in vehicle: use
`MOVEMENTFLAG_ONTRANSPORT` as an indicator that the unit is in the
vehicle.

Also, update `UpdateMovementState` method with simplified checks and the
updated logic (common > rare).

This should fix issues:
https://github.com/mod-playerbots/mod-playerbots/issues/1903 and
https://github.com/mod-playerbots/mod-playerbots/issues/1902

NOTE: The `PlayerbotAI` class has a method `CanMove` with the same
checks, but this method is only used once in the code. We should decide
how to properly check if the bot can move or not:

1. Place all logic into `IsMovingAllowed` and drop `CanMove`.
2. Place all logic into `CanMove` and use it inside `IsMovingAllowed`.
3. Use them for different approaches: 
- `CanMove`: simple checks (unit flags, CC state, death state, travel
state, vehicle state);
- `IsMovingAllowed`: everything from `CanMove` + MM flags checks (not
sure about rooted since it still checks for movement flags...).
2025-12-15 15:32:49 +01:00
Crow
934e73ae20 Add Ogri'la and Blackwind Landing to PvP Restricted Areas (#1915)
This PR adds a couple of neutral quest hubs in Outland to PvP restricted
areas (and makes a couple of very minor formatting fixes to the Karazhan
files).

3786: Ogri'la
3973: Blackwind Landing (Sha'tari Skyguard quest hub in Skettis)
2025-12-15 15:26:38 +01:00
Crow
f4b4d8967f Karazhan Refactor + Nightbane Strategy (#1847)
This PR completely refactors the Karazhan raid strategies to clean up
the code and polish up/introduce new strategies, including for
Nightbane.

General changes with respect to the code:
- Moved gating checks for action methods to triggers instead of relying
on isUseful functions
- Got rid of the nonsensical helper class I made and switched to a
helper namespace
- Broke up some longer action classes into separate methods and/or
private members
- Deleted/consolidated some excess code
- Made greater use of early returns
- Renamed methods, multipliers, and triggers to make them easier to
understand
- Generally made edits to conform with AC code standards

Below are the implemented strategies. I’m including all of them, not
just new/modified ones, because I don’t think the first set of
strategies was ever tested. So each boss could probably use complete
testing.

### **Trash**

I added strategies for a trash mob that I find particularly annoying:
- Mana Warp: These are, IMO, the most annoying trash mobs in Karazhan.
They blow up when low on health, and having two blow up in quick
succession is enough to take out a decent chunk of your raid. The
strategy directs bots to use pretty much every stun in the book on Mana
Warps when they’re low on HP, which is the only way to prevent the
explosion.

### **Attumen**

- During the first phase, bots will focus on Midnight (taking either
Midnight or Attumen to 25% starts the next phase). When Attumen spawns,
the assist tank will pick him up and move him away so that bots don’t
get cleaved.
- When Attumen mounts Midnight, starting phase 2, threat is wiped, and
bots will pause DPS for a few seconds to allow the main tank to get
aggro. All bots, other than the main tank and any bot that pulls aggro,
will stack behind Attumen (~6 yards for ranged so Hunters can still
attack).

### **Moroes**

- As before, bots will mark and prioritize adds and the boss in the
recommended kill order: Dorothea, Catriona, Keira, Rafe, Robin, Crispin,
and Moroes. In practice, the enemies will probably be stacked up, and
the bots will AoE them down in accordance with their typical AoE
strategies, but classes without AoE capabilities should still prioritize
the skull.
- Based on testing feedback, added a method for the main tank to
prioritize Moroes

### **Maiden of Virtue**

I’ve made only minor changes to Revision’s original strategy here. 

- The tank with aggro will position Maiden in the middle of the room and
move her to a healer when Repentance is cast so that the Holy Ground
will break the healer’s stun.
- Ranged bots have assigned positions between the columns around the
middle of the room (to prevent chain damage from Holy Wrath).

### **The Big Bad Wolf**

- The tank with aggro brings the boss to the front left of the stage. 
- If a bot gets turned into Little Red Riding Hood, it will run around
the stage in a counter-clockwise rectangle until the transformation
fades. I tweaked this strategy a bit; it's still not perfect, but it
works better than before.

### **Romulo and Julianne**

There are no substantive changes to this strategy. As before, in phase
3, when both bosses are active, the bots switch back-and-forth between
the bosses by alternating the skull icon based on which boss has lower
HP (10% differential).

### **The Wizard of Oz**

There are no substantive changes to this strategy. As before, bots mark
the bosses with a skull icon in their recommended kill order: Dorothee,
Tito (assuming he spawns before you kill Dorothee), Roar, Strawman,
Tinhead, and the Crone. Additionally, Mages will spam Scorch on Strawman
to daze him.

### **The Curator**

- The tank will drag the boss to a fixed spot down the hallway. Ranged
bots will spread out to avoid chain damage from Arcing Sear, and bots
will mark Astral Flares with the skull icon to prioritize them down.
- Those strategies already existed, but now I made the assist tank also
focus on the boss to try to stay second in aggro and therefore absorb
Hateful Bolts.
- Added a multiplier to save Bloodlust/Heroism until Evocation.

### **Terestian Illhoof**

There are no substantive changes to this strategy. The bots will mark
targets with the skull icon in the following order: Demonic Chains,
Kil'rek, and Illhoof.

### **Shade of Aran**
I redid the strategies a bit, and I think (hope) they work better.

- Flame Wreath: Bots will stop moving until the aura fades. There is a
bug in which Flame Wreath will sometimes persist long beyond its
supposed 20-second duration. If Aran casts Arcane Explosion during this
time, you will almost certainly wipe, so that’s frustrating. I made it
so that bots will stay in place still and eat the Arcane Explosion
because it’s the lesser of two evils, and if you are overgeared, you may
be able to survive. In the previous strategy, bots would stop actions
entirely, but now they should keep attacking/casting without moving.
- Arcane Explosion: No substantive changes here--bots will run out
immediately and stay out of range until the cast finishes.
- Conjured Elementals: No substantive changes here--they will be marked
one-by-one by the skull icon, except that the marking will skip any
elemental that is banished.
- Ranged Positioning: I redid this strategy. Ranged bots will now
maintain a distance between 11 and 15 yards from the boss. This keeps
them out of the 10-yard radius in which Aran silences while also keeping
them from getting too far away and getting stuck in the alcoves.

### **Netherspite**

I significantly refactored the action methods here, but substantively
the original strategy remains mostly intact.

- Red (Tank) Beam: One tank will be assigned to block the beam for each
Portal Phase. The assigned tank will dance in and out of the beam (5
seconds in, 5 seconds out). Tanks intentionally do not avoid Void Zones
(it was the lesser of two evils for them to take that damage vs. trying
to dynamically avoid them, moving the boss, and possibly getting
everybody out of position.
- Blue (DPS) Beam: DPS other than Rogues and Warriors are eligible to be
assigned (one-by-one) to block this beam. When the assigned blocker
reaches 25 stacks of the debuff, they will leave the beam, and the next
assigned blocker will take their place. If a Void Zone drops under the
assigned blocker, the bot will move along the beam to get out of the
Void Zone so that they do not stop blocking.
- Green (Healer) Beam: This works the same way as the Blue Beam, except
that eligible blockers are healers, Rogues, and DPS Warriors. Healers
that are assigned to block will swap in the same way as Blue Beam
blockers. If a Rogue or DPS Warrior is the assigned blocker, however,
they will stand in the beam for the entire Portal Phase since they do
not suffer any adverse effects from the beam. In this PR, I made the
strategy prioritize Rogues and DPS Warriors over healers to try to avoid
the need for bots to swap (and to avoid the irritating scenario in which
a healer would block the beam for the first half of a phase and then tag
in a Rogue or DPS Warrior, which would be wasted by blocking only half
of a phase).
- Non-Blockers: They will stay at least 5 yards away from each beam
until called to be an assigned blocker. They will also avoid Void Zones.
- Banish Phase: The only strategy I implemented was for bots to avoid
residual Void Zones from the Portal Phase.
- Phase Transitions: Bots should pause DPS at the beginning of the
encounter and whenever Netherspite transitions back from the Banish
Phase to the Portal Phase (which is an aggro reset). Note that this
doesn't wipe DOTs, and there's not much I can do about that.

### **Prince Malchezaar**

The action methods are significantly refactored, but the strategy
substantively is not changed very much.

- Bots will maintain distance from Infernals. The tank has a larger
avoidance radius to give DPS a little bit of margin to work with.
Depending on Infernal placement, it is possible for bots to get stuck in
some bad positions. In that case, the best solution is to put them on
“flee” and lead them to a better position.
- Bots that get Enfeebled will run out of Shadow Nova range. They should
pick a path that does not cross within any Infernal's Hellfire radius.
- Added a multiplier to save Bloodlust/Heroism until Phase 3.

### **Nightbane**

**Disclaimer**: Bots are terrible at this encounter, in large part
because the map is awful (even before the recent Core changes). So the
strategies are not ideal because they need to operate within the bots’
limitations. I STRONGLY suggest you clear the entire Livery Stables
(including the upper level) because the mobs in them have a high risk of
pulling through the floor of the Master’s Terrace. Ideally, you should
clear out the Scullery too.

The strategy uses waypoints toward the Northeastern door to the Master’s
Terrace. I tried several different locations, and that worked best for
me based on where Nightbane lands (note that he has a different landing
spot for the encounter start vs. the start subsequent ground phases).

- Ground Phase, main tank: The main tank uses two waypoints after it
picks up the boss—the movement pattern should be kind of like a reverse
checkmark, where the tank moves back along the inner edge of the
terrace, then pivots and moves at a bit of an angle to the outer edge. I
did this as a way to get the tank to face Nightbane sideways across the
terrace, which is how he’s supposed to be tanked. The main tank will not
get out of Charred Earth. This is intended. The tank cannot move
dynamically enough to avoid it while also not turning the boss and
wiping the raid.
- Ground phase, ranged: Ranged bots rotate between three waypoints. They
start stacked at the same position. If Charred Earth is dropped on that
position, the bots will rotate to the second position. If Charred Earth
is dropped on that position, they will rotate to the third position. The
maximum number of Charred Earths that can be active is two, so if one is
dropped on the third position, the ranged bots should rotate back to the
first position.
- Ground Phase, other melee: Melee bots have no coded Charred Earth
avoidance strategy. They do decently enough with the general “avoid aoe”
strategy.
- Flight Phase: Bots move to Nightbane’s flight position—Nightbane is
bugged and cannot be attacked in the air in AC, but all bots still need
to stay near him or he will wipe the raid with Fireball Barrage. Bots
stack on the same position; when Rain of Bones is cast (one time,
snapshotted on the target’s location), all bots will move away to a
second nearby position. They will then kill the Restless Skeletons. The
Flight Phase lasts for 45 seconds, but Nightbane emotes after 35 seconds
and goes to land—during the final 10-second period, bots are freed from
all strategies and will follow the master. You need to lead them back to
the Northeastern part of the terrace before Nightbane lands so that bots
can get immediately positioned when he lands.
- Phase Changes: Whenever Nightbane lands, bots should pause DPS for the
main tank to get aggro.
- Managing bots/pets: During the Flight Phase, bots and pets have a
tendency to chase Nightbane outside of the boundaries of the map and
pull mobs from other parts of the instance as well as cause Restless
Skeletons to spawn out of bounds (and then further cause bots/pets to go
out of bounds to attack the skeletons). The strategy should solve for
this, but if there are still issues, it should be noted. My resolution
was to implement the following: (1) when Nightbane takes flight, bots
stop attacking and mark him with the moon icon, and Hunters and Warlocks
put pets on passive and recall them (they will put pets on defensive
again when Nightbane lands), and (2) all temporary pets are disabled for
the duration of the fight (Water Elementals, Shaman wolves, Druid
treants, etc.).

**Known Issues:**

- The approach to getting Nightbane to turn sideways is not always spot
on since it’s a workaround to account for what should be dynamic
movement. He can end up at a bit of an angle. I’ve tweaked the positions
such that if he is at an angle, it should not be a situation in which
any ranged position is exposed to Smoldering Breath or Cleave. That does
increase the risk that poor positioning may expose a ranged position to
Tail Sweep. This is obviously suboptimal, but that attack is much more
survivable.
- Ranged bots move between the three waypoints by reading the presence
of the Charred Earth aura on themselves. Sometimes, a bot may reach the
next waypoint with the Charred Earth aura from the previous waypoint
still on them (depending on timing of ticks), in which case the bot will
keep moving to the next waypoint. This can cause an issue in which the
ranged group gets split. So you could have Charred Earth dropped on
position 1, and some ranged bots will go to position 2 but others will
go to position 3, and then if the second Charred Earth is dropped on
position 3, the bots at position 3 will move back to position 1 (which
is still in Charred Earth) before then moving to position 2. Balancing
spacing between waypoints, positioning with respect to the boss, line of
sight, and maximum distance is very difficult to do. I messed with the
positioning a lot, and I am not sure if this possibility cannot be
entirely avoided without creating other issues. I have at least reached
a result in which I have not seen any bots cycle indefinitely (though if
testing observes this, it is obviously a problem).

Ultimately, I wouldn’t say I’m totally satisfied with the strategy.
Wipes are still possible on account of bad RNG. But I think it does make
the fight a lot more manageable, as it is a fight that is very difficult
with IP nerfs in the absence of any strategy. Previously, I needed to
bring 3 healers, and they would still all be out of mana by the first
Flight Phase and reliant on MP5 for the remainder of the fight, and I
would wipe multiple times before a kill.
2025-12-10 21:08:25 +01:00
Nicolas Lebacq
910b8a9c53 fix: Made bots roll in a more reasonable time on group loots. (#1857)
# Description

This PR changes the way loot rolls are being evaluated.

It puts a maximum priority on the loot action so it does not hang for so
long.
2025-12-09 10:29:57 -08:00
Tecc
bb569b4d39 Fix: Arena - PersonalRating and MMR issue for bot teams (#1789)
# Fix: Arena PersonalRating and MMR issue for bot teams

## Problem
Bot arena teams are created with artificial random ratings (1000-2000
range), but when bots join these teams, their personal ratings and
matchmaker ratings (MMR) use default config values instead of being
adjusted to match the team's artificial rating. This causes matchmaking
issues since the system uses personal ratings for queue calculations.

## Root Cause
The issue occurred because `SetRatingForAll()` was called during team
creation but only affected the captain. When additional bots were added
later via `AddMember()`, they received default values from
`CONFIG_ARENA_START_PERSONAL_RATING` and
`CONFIG_ARENA_START_MATCHMAKER_RATING` instead of values appropriate for
the team's artificial rating.

## Solution
After bots are added to arena teams, the fix:

1. Uses `SetRatingForAll()` to align all personal ratings with team
rating
2. Adjusts matchmaker ratings based on team context vs default
configuration
3. Saves changes to both database tables with proper data types

## Impact
- Personal ratings now match team ratings for artificial bot teams
- MMR values are adjusted for artificial bot team ratings instead of
using default config values
- Arena matchmaking functions correctly for bot teams with random
ratings
- Only affects new arena team assignments after deployment
- Existing player teams and normal config behavior are unaffected

## Manual Database Update

For existing installations, the provided SQL script could be used to fix
bot teams created before this patch.

### Update personal rating
```sql
UPDATE arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
SET atm.personalRating = at.rating
WHERE a.username LIKE 'rndbot%'
  AND atm.personalRating != at.rating;
```

### Update MMR for existing entries
```sql
UPDATE character_arena_stats cas
JOIN characters c ON cas.guid = c.guid
JOIN auth.account a ON c.account = a.id
JOIN arena_team_member atm ON cas.guid = atm.guid
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
SET
    cas.matchMakerRating = GREATEST(at.rating, 1500),  -- Use team rating or 1500 minimum
    cas.maxMMR = GREATEST(cas.maxMMR, cas.matchMakerRating)  -- Update maxMMR if needed
WHERE
    a.username LIKE '%rndbot%'
    AND (
        -- Update if MMR doesn't match team context
        (at.rating > 1500 AND cas.matchMakerRating < at.rating) OR
        (at.rating <= 1500 AND cas.matchMakerRating != 1500) OR
        cas.matchMakerRating IS NULL
    )
    AND (
        -- Map arena team type to character_arena_stats slot
        (at.type = 2 AND cas.slot = 0) OR  -- 2v2 teams use slot 0
        (at.type = 3 AND cas.slot = 1) OR  -- 3v3 teams use slot 1
        (at.type = 5 AND cas.slot = 2)     -- 5v5 teams use slot 2
    );
```

### Insert missing MMR records for bots without character_arena_stats
entries
```sql
INSERT INTO character_arena_stats (guid, slot, matchMakerRating, maxMMR)
SELECT
    atm.guid,
    CASE
        WHEN at.type = 2 THEN 0  -- 2v2 -> slot 0
        WHEN at.type = 3 THEN 1  -- 3v3 -> slot 1
        WHEN at.type = 5 THEN 2  -- 5v5 -> slot 2
        ELSE 0
    END as slot,
    GREATEST(at.rating, 1500) as matchMakerRating,
    GREATEST(at.rating, 1500) as maxMMR
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
WHERE
    a.username LIKE '%rndbot%'
    AND NOT EXISTS (
        SELECT 1 FROM character_arena_stats cas2
        WHERE cas2.guid = atm.guid
        AND cas2.slot = CASE
            WHEN at.type = 2 THEN 0
            WHEN at.type = 3 THEN 1
            WHEN at.type = 5 THEN 2
            ELSE 0
        END
    )
    AND at.rating > 0;
```

## Related issues

Fixes: #1787 
Fixes: #1800


## Verification Queries

### Query 1: Check personal rating alignment
```sql
SELECT
    'Personal Rating Check' as check_type,
    COUNT(*) as total_bot_members,
    SUM(CASE WHEN atm.personalRating = at.rating THEN 1 ELSE 0 END) as correct_ratings,
    SUM(CASE WHEN atm.personalRating != at.rating THEN 1 ELSE 0 END) as incorrect_ratings,
    ROUND(AVG(at.rating), 2) as avg_team_rating,
    ROUND(AVG(atm.personalRating), 2) as avg_personal_rating
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
WHERE
    a.username LIKE '%rndbot%';
```

### Query 2: Check MMR alignment
```sql
SELECT
    'MMR Alignment Check' as check_type,
    COUNT(*) as total_mmr_records,
    SUM(CASE
        WHEN at.rating > 1500 AND cas.matchMakerRating >= at.rating THEN 1
        WHEN at.rating <= 1500 AND cas.matchMakerRating = 1500 THEN 1
        ELSE 0
    END) as correct_mmr,
    SUM(CASE
        WHEN at.rating > 1500 AND cas.matchMakerRating < at.rating THEN 1
        WHEN at.rating <= 1500 AND cas.matchMakerRating != 1500 THEN 1
        ELSE 0
    END) as incorrect_mmr,
    ROUND(AVG(at.rating), 2) as avg_team_rating,
    ROUND(AVG(cas.matchMakerRating), 2) as avg_mmr,
    ROUND(AVG(cas.maxMMR), 2) as avg_max_mmr
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
JOIN character_arena_stats cas ON atm.guid = cas.guid
WHERE
    a.username LIKE '%rndbot%'
    AND (
        (at.type = 2 AND cas.slot = 0) OR
        (at.type = 3 AND cas.slot = 1) OR
        (at.type = 5 AND cas.slot = 2)
    );
```

### Query 3: Detailed team-by-team analysis
```sql
SELECT
    at.arenaTeamId,
    at.name as team_name,
    at.type as team_type,
    at.rating as team_rating,
    COUNT(atm.guid) as member_count,
    GROUP_CONCAT(DISTINCT atm.personalRating) as personal_ratings,
    GROUP_CONCAT(DISTINCT cas.matchMakerRating) as mmr_values,
    CASE
        WHEN COUNT(DISTINCT atm.personalRating) = 1 AND MIN(atm.personalRating) = at.rating THEN 'OK'
        ELSE 'MISMATCH'
    END as personal_rating_status,
    CASE
        WHEN COUNT(DISTINCT cas.matchMakerRating) = 1 AND (
            (at.rating > 1500 AND MIN(cas.matchMakerRating) >= at.rating) OR
            (at.rating <= 1500 AND MIN(cas.matchMakerRating) = 1500)
        ) THEN 'OK'
        ELSE 'MISMATCH'
    END as mmr_status
FROM arena_team at
JOIN arena_team_member atm ON at.arenaTeamId = atm.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
LEFT JOIN character_arena_stats cas ON atm.guid = cas.guid
    AND cas.slot = CASE
        WHEN at.type = 2 THEN 0
        WHEN at.type = 3 THEN 1
        WHEN at.type = 5 THEN 2
        ELSE 0
    END
WHERE
    a.username LIKE '%rndbot%'
GROUP BY at.arenaTeamId, at.name, at.type, at.rating
ORDER BY at.rating DESC;
```
2025-12-08 12:35:06 +01:00
NoxMax
dde16674c3 Fix: Stop pets from fighting in PVP prohibited zones (#1829)
Stripped down version of #1818. No new features. Refactors
IsPossibleTarget in AttackersValue.cpp to a better style and makes sure
pets don't attack in prohibited zones.

Testing:
Confirmed that aggro pets no longer attack in PVP prohibited areas, but
still do outside them. Zim'Torga in Zul'Drak is a good example to test
this (ID 4323). Lookout for death knights with a Risen Ally
(uncontrolled and naturally aggro) now they respect PVP prohibition like
their master.

Note: If you manually teleport a bot that is in mid combat to a PVP
prohibited area, its aggro pet might still attack, because its master is
still in combat strategy. Otherwise the pet will not attack if its
master has switched to non-combat.
2025-12-08 12:34:16 +01:00
HennyWilly
e5b2791053 Improve Molten Core Strategy (#1852)
This is my first attempt of implementing playerbot strategies.

A team of 40 can steamroll Molten Core relatively easy, even with lower
item levels.
Regardless, this PR adds resistance triggers and actions to mitigate
some damage.
Additionally, improvements were made for the encounters with Garr, Baron
Geddon, Shazzrah and Golemagg.
A short summary per boss is listed below.
All planned features are included, but feedback is of course
appreciated.

### Lucifron
- Shadow resistance: mitigate damage from [Impending
Doom](https://www.wowhead.com/classic/spell=19702/impending-doom) and
[Shadow
Shock](https://www.wowhead.com/classic/spell=19460/shadow-shock).

### Magmadar
- Fire resistance: mitigate damage from [Lava
Bomb](https://www.wowhead.com/classic/spell=19411/lava-bomb) and [Magma
Spit](https://www.wowhead.com/classic/spell=19450/magma-spit).
- Like King Dred and the fraction commander (Nexus), this fight might
profit from an anti-fear strategy in order to counter
[Panic](https://www.wowhead.com/classic/spell=19408/panic). Not
implemented here.

### Gehennas
- Shadow resistance: mitigate damage from [Shadow
Bolt](https://www.wowhead.com/classic/spell=19728/shadow-bolt) and
increase the chance to resist [Gehennas'
Curse](https://www.wowhead.com/classic/spell=19716/gehennas-curse).

### Garr
- Fire resistance: mitigate damage from the Firesworn adds
([Immolate](https://www.wowhead.com/classic/spell=20294/immolate) and
[Eruption](https://www.wowhead.com/classic/spell=19497/eruption)).
- Disabled dps aoe abilities via multiplier. This one is important
because multiple exploding adds at once might delete bots rather
quick...

### Baron Geddon
- Refactored the existing strategy.
- Fire resistance: mitigate damage from [Ignite
Mana](https://www.wowhead.com/classic/spell=19659/ignite-mana),
[Inferno](https://www.wowhead.com/classic/spell=19695/inferno) and
[Living Bomb](https://www.wowhead.com/classic/spell=20475/living-bomb).
- Better Inferno handling: Before moving away, bots stop attacking and
interrupt their spells. Additionally, the new multiplier prevents bots
from running back to Geddon while Inferno is still active.

### Shazzrah
- Ranged bots now position themselves in a sweet spot that prevents them
from getting hit with [Arcane
Explosion](https://www.wowhead.com/classic/spell=19712/arcane-explosion)
but still close enough to dps and heal.

### Sulfuron Harbinger
- Fire resistance: mitigate damage from [Hand of
Ragnaros](https://www.wowhead.com/classic/spell=19780/hand-of-ragnaros)
and [Immolate](https://www.wowhead.com/classic/spell=20294/immolate). To
be fair, this one is quite negligible...

### Golemagg
- Fire resistance: mitigate damage from [Magma
Splash](https://www.wowhead.com/classic/spell=13880/magma-splash) and
[Pyroblast](https://www.wowhead.com/classic/spell=20228/pyroblast).
- Disabled dps aoe abilities via multiplier. Kind of a preference on my
side. Otherwise, the Core Ragers spam emotes about not wanting to die.

### Majordomo Executus
- Shadow resistance: mitigate damage from [Aegis of
Ragnaros](https://www.wowhead.com/classic/spell=20620/aegis-of-ragnaros),
[Shadow Shock](https://www.wowhead.com/classic/spell=20603/shadow-shock)
and [Shadow
Bolt](https://www.wowhead.com/classic/spell=21077/shadow-bolt). This one
is also negligible, TBF.

### Ragnaros
- Fire resistance: mitigate damage from [Wrath of
Ragnaros](https://www.wowhead.com/classic/spell=20566/wrath-of-ragnaros)
and [Lava
Burst](https://www.wowhead.com/classic/spell=21158/lava-burst).
2025-12-08 12:29:07 +01:00
Keleborn
353c29dfc4 Bug: Fix bots leaving LFG groups before master (#1876)
I removed bots checking if they should leave group every tick, and will
rely on the LeaveGroupFarAway action. I also increased the timer from 5
seconds to 20 seconds. No need to check this that often.
2025-12-08 12:25:40 +01:00
Keleborn
52c3e96641 Rename groupmaster to groupleader and related variables. (#1875)
Fix the naming conventions. 
Master should be reserved to identify a bots Master. 
groupleaders are not necessarily group masters and it should be clear
what the bot is looking for. (In most solo cases leader=master)
2025-12-03 13:25:01 +01:00
Crow
38e2d8584b Add missing break in ApplyInstanceStrategies (#1887)
Well, I hope nobody has tried Magtheridon lately. It looks like this got
inadvertently deleted during the merge.
2025-11-28 19:00:12 +01:00
Keleborn
d5dbc4ddd7 Hotfix: prevent server crash when whisper 'logout' (#1874)
Temp Hotfix to resolve #1870.
2025-11-24 21:49:55 +01:00
Keleborn
2424f73bc4 Core Merge PR - Replace OnPlayerChat with OnPlayerCanUseChat (#1838)
First stab at getting this working. Im not sure if Im missing something,
but it seemed to be a pretty simple change overall.

Based on testing the bots do respond to commands via whisper and group.

Edit: Relevant PR this addresses.

50f8f145d2 (diff-baadebd8cd1117ca48225f316a5ab3fd5fd55b20963394d302341147183db067)
2025-11-23 20:45:31 +01:00
1259 changed files with 19459 additions and 15696 deletions

View File

@@ -25,21 +25,25 @@ jobs:
with: with:
repository: 'mod-playerbots/azerothcore-wotlk' repository: 'mod-playerbots/azerothcore-wotlk'
ref: 'Playerbot' ref: 'Playerbot'
path: 'ac'
- name: Checkout Playerbot Module - name: Checkout Playerbot Module
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: 'mod-playerbots/mod-playerbots' repository: 'mod-playerbots/mod-playerbots'
path: 'modules/mod-playerbots' #path: 'modules/mod-playerbots'
path: ac/modules/mod-playerbots
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.13 uses: hendrikmuhs/ccache-action@v1.2.13
- name: Configure OS - name: Configure OS
shell: bash shell: bash
working-directory: ac
env: env:
CONTINUOUS_INTEGRATION: true CONTINUOUS_INTEGRATION: true
run: | run: |
./acore.sh install-deps ./acore.sh install-deps
- name: Build - name: Build
shell: bash shell: bash
working-directory: ac
run: | run: |
export CTOOLS_BUILD=all export CTOOLS_BUILD=all
./acore.sh compiler build ./acore.sh compiler build

View File

@@ -21,10 +21,11 @@
# THRESHOLDS # THRESHOLDS
# QUESTS # QUESTS
# COMBAT # COMBAT
# PALADIN BUFFS STRATEGIES # GREATER BUFFS STRATEGIES
# CHEATS # CHEATS
# SPELLS # SPELLS
# FLIGHTPATH # FLIGHTPATH
# PROFESSIONS
# RANDOMBOT-SPECIFIC SETTINGS # RANDOMBOT-SPECIFIC SETTINGS
# GENERAL # GENERAL
# LEVELS # LEVELS
@@ -44,7 +45,7 @@
# HUNTER # HUNTER
# ROGUE # ROGUE
# PRIEST # PRIEST
# DEATHKNIGHT # DEATH KNIGHT
# SHAMAN # SHAMAN
# MAGE # MAGE
# WARLOCK # WARLOCK
@@ -55,7 +56,7 @@
# HUNTER # HUNTER
# ROGUE # ROGUE
# PRIEST # PRIEST
# DEATHKNIGHT # DEATH KNIGHT
# SHAMAN # SHAMAN
# MAGE # MAGE
# WARLOCK # WARLOCK
@@ -90,17 +91,20 @@ AiPlayerbot.MinRandomBots = 500
AiPlayerbot.MaxRandomBots = 500 AiPlayerbot.MaxRandomBots = 500
# Randombot accounts # Randombot accounts
# If you are not using any expansion at all, you may have to set this manually, in which case please ensure that RandomBotAccountCount is at least greater than (MaxRandomBots / 10 + AddClassAccountPoolSize) # If you are not using any expansion at all, you may have to set this manually, in which case please
# ensure that RandomBotAccountCount is at least greater than (MaxRandomBots / 10 + AddClassAccountPoolSize)
# Default: 0 (automatic) # Default: 0 (automatic)
AiPlayerbot.RandomBotAccountCount = 0 AiPlayerbot.RandomBotAccountCount = 0
# Delete all randombot accounts # Delete all randombot accounts
# To apply this, set the number to 1 and run the Worldserver. Once deletion is complete, if you would like to recreate randombots, set the number back to 0 and rerun the Worldserver. # To apply this, set the number to 1 and run the Worldserver. Once deletion is complete, if you would
# like to recreate randombots, set the number back to 0 and rerun the Worldserver.
AiPlayerbot.DeleteRandomBotAccounts = 0 AiPlayerbot.DeleteRandomBotAccounts = 0
# Disable randombots when no real players are logged in # Disable randombots when no real players are logged in
# Default: 0 (randombots will login when server starts) # Default: 0 (randombots will login when server starts)
# If enabled, randombots will only log in 30 seconds (default) after a real player logs in, and will log out 300 seconds (default) after all real players log out # If enabled, randombots will only log in 30 seconds (default) after a real player logs in, and will
# log out 300 seconds (default) after all real players log out
AiPlayerbot.DisabledWithoutRealPlayer = 0 AiPlayerbot.DisabledWithoutRealPlayer = 0
AiPlayerbot.DisabledWithoutRealPlayerLoginDelay = 30 AiPlayerbot.DisabledWithoutRealPlayerLoginDelay = 30
AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300 AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300
@@ -152,7 +156,8 @@ AiPlayerbot.AllowGuildBots = 1
AiPlayerbot.AllowTrustedAccountBots = 1 AiPlayerbot.AllowTrustedAccountBots = 1
# Randombots will create guilds with nearby randombots # Randombots will create guilds with nearby randombots
# Note: currently, randombots will not invite more bots after a guild is created (i.e., randombot guilds will have only the 10 initial randombots needed to sign the charter) # Note: currently, randombots will not invite more bots after a guild is created,
# meaning randombot guilds will have only the 10 initial randombots needed to sign the charter
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.RandomBotGuildNearby = 0 AiPlayerbot.RandomBotGuildNearby = 0
@@ -186,7 +191,8 @@ AiPlayerbot.AutoInitOnly = 0
# Default: 1.0 (same with the player) # Default: 1.0 (same with the player)
AiPlayerbot.AutoInitEquipLevelLimitRatio = 1.0 AiPlayerbot.AutoInitEquipLevelLimitRatio = 1.0
# Bot automatically trains spells when talking to trainer (yes = train all available spells as long as the bot has the money, free = auto trains with no money cost, no = only list spells) # Bot automatically trains spells when talking to trainer
# yes = train all available spells as long as the bot has the money, free = auto trains with no money cost, no = only list spells
AiPlayerbot.AutoTrainSpells = yes AiPlayerbot.AutoTrainSpells = yes
# #
@@ -263,7 +269,7 @@ AiPlayerbot.UseFastFlyMountAtMinLevel = 70
AiPlayerbot.RandomBotShowHelmet = 1 AiPlayerbot.RandomBotShowHelmet = 1
AiPlayerbot.RandomBotShowCloak = 1 AiPlayerbot.RandomBotShowCloak = 1
# Randombots and altbots automatically equip upgrades (bots will equip any item obtained from looting or a quest if they are sufficient upgrades) # Randombots and altbots automatically equip any items in their inventory that are sufficient upgrades
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoEquipUpgradeLoot = 1 AiPlayerbot.AutoEquipUpgradeLoot = 1
@@ -311,7 +317,8 @@ AiPlayerbot.GlobalCooldown = 500
# Max wait time when moving # Max wait time when moving
AiPlayerbot.MaxWaitForMove = 5000 AiPlayerbot.MaxWaitForMove = 5000
# Disable use of MoveSplinePath for bot movement, will result in more erratic bot movement but means stun/snare/root/etc will work on bots (they wont reliably work when MoveSplinePath is enabled, though slowing effects still work ok) # Enable/disable use of MoveSplinePath for bot movement
# Disabling will result in more erratic movement but is required for stuns, snares, and roots to work on bots
# Default: 0 - MoveSplinePath enabled # Default: 0 - MoveSplinePath enabled
# 1 - MoveSplinePath disabled in BG/Arena only # 1 - MoveSplinePath disabled in BG/Arena only
# 2 - MoveSplinePath disabled everywhere # 2 - MoveSplinePath disabled everywhere
@@ -405,10 +412,11 @@ AiPlayerbot.HighMana = 65
# #
# #
# Bots pick their quest rewards (yes = picks the most useful item, no = list all rewards, ask = pick useful item and lists if multiple) # Bots pick their quest rewards
# yes = picks the most useful item, no = list all rewards, ask = pick useful item and lists if multiple
AiPlayerbot.AutoPickReward = yes AiPlayerbot.AutoPickReward = yes
# Sync quests with player (Bots will complete quests the moment you hand them in and will not loot quest items.) # Sync quests with player (bots will complete quests the moment you hand them in and will not loot quest items.)
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.SyncQuestWithPlayer = 1 AiPlayerbot.SyncQuestWithPlayer = 1
@@ -433,7 +441,7 @@ AiPlayerbot.DropObsoleteQuests = 1
# Auto add dungeon/raid strategies when entering the instance if implemented # Auto add dungeon/raid strategies when entering the instance if implemented
AiPlayerbot.ApplyInstanceStrategies = 1 AiPlayerbot.ApplyInstanceStrategies = 1
# Enable auto avoid aoe strategy (experimental) # Enable auto avoid aoe strategy
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoAvoidAoe = 1 AiPlayerbot.AutoAvoidAoe = 1
@@ -460,7 +468,7 @@ AiPlayerbot.FleeingEnabled = 1
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# PALADIN BUFFS STRATEGIES # GREATER BUFFS STRATEGIES
# #
# #
@@ -483,12 +491,13 @@ AiPlayerbot.RPWarningCooldown = 30
# #
# Enable/Disable maintenance command # Enable/Disable maintenance command
# Learn all available spells and skills, refresh consumables, repair, enchant equipment and socket gems if bot's level is above AiPlayerbot.MinEnchantingBotLevel # Learn all available spells and skills, assign talent points, refresh consumables, repair, enchant equipment, socket gems, etc.
# Applies if bot's level is above AiPlayerbot.MinEnchantingBotLevel
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.MaintenanceCommand = 1 AiPlayerbot.MaintenanceCommand = 1
# Enable/Disable specific maintenance command functionality for alt bots # Enable/Disable specific maintenance command functionality for alt bots
# Disable to prevent players from giving free bags, spells, skill levels etc to their alt bots # Disable to prevent players from giving free bags, spells, skill levels, etc. to their alt bots
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AltMaintenanceAmmo = 1 AiPlayerbot.AltMaintenanceAmmo = 1
AiPlayerbot.AltMaintenanceFood = 1 AiPlayerbot.AltMaintenanceFood = 1
@@ -500,6 +509,7 @@ AiPlayerbot.AltMaintenanceBags = 1
AiPlayerbot.AltMaintenanceMounts = 1 AiPlayerbot.AltMaintenanceMounts = 1
AiPlayerbot.AltMaintenanceSkills = 1 AiPlayerbot.AltMaintenanceSkills = 1
# "Special Spells" consist of any spells listed in AiPlayerbot.RandomBotSpellIds and Death Gate for Death Knights
AiPlayerbot.AltMaintenanceClassSpells = 1 AiPlayerbot.AltMaintenanceClassSpells = 1
AiPlayerbot.AltMaintenanceAvailableSpells = 1 AiPlayerbot.AltMaintenanceAvailableSpells = 1
AiPlayerbot.AltMaintenanceSpecialSpells = 1 AiPlayerbot.AltMaintenanceSpecialSpells = 1
@@ -514,8 +524,8 @@ AiPlayerbot.AltMaintenanceReputation = 1
AiPlayerbot.AltMaintenanceAttunementQuests = 1 AiPlayerbot.AltMaintenanceAttunementQuests = 1
AiPlayerbot.AltMaintenanceKeyring = 1 AiPlayerbot.AltMaintenanceKeyring = 1
# Enable/Disable autogear command, which automatically upgrades bots' gear
# Enable/Disable autogear command, which automatically upgrades bots' gear; the quality is limited by AutoGearQualityLimit and AutoGearScoreLimit # The quality is limited by AutoGearQualityLimit and AutoGearScoreLimit
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoGearCommand = 1 AiPlayerbot.AutoGearCommand = 1
@@ -579,6 +589,30 @@ AiPlayerbot.BotTaxiGapJitterMs = 100
# #
#################################################################################################### ####################################################################################################
####################################################################################################
# PROFESSIONS
# Note: Random bots currently do not get professions
#
# Automatically adds the 'master fishing' strategy to bots that have the fishing skill when the bots master fishes.
# Default: 1 (Enabled)
AiPlayerbot.EnableFishingWithMaster = 1
# Distance from itself (in yards) that a bot with a master will search for water to fish
AiPlayerbot.FishingDistanceFromMaster = 10.0
# Distance from itself (in yards) that a bot without a master will search for water to fish
# Currently not relevant since masterless bots will not fish
AiPlayerbot.FishingDistance = 40.0
# Distance from water (in yards) beyond which a bot will remove the 'master fishing' strategy
AiPlayerbot.EndFishingWithMaster = 30.0
#
#
#
####################################################################################################
####################################### #######################################
# # # #
# RANDOMBOT-SPECIFIC SETTINGS # # RANDOMBOT-SPECIFIC SETTINGS #
@@ -630,7 +664,7 @@ AiPlayerbot.RandomBotHordeRatio = 50
AiPlayerbot.DisableDeathKnightLogin = 0 AiPlayerbot.DisableDeathKnightLogin = 0
# Enable simulated expansion limitation for talents and glyphs # Enable simulated expansion limitation for talents and glyphs
# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 # If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61
# and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71 # and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.LimitTalentsExpansion = 0 AiPlayerbot.LimitTalentsExpansion = 0
@@ -700,7 +734,7 @@ AiPlayerbot.RandomGearQualityLimit = 3
# Max iLVL Phase 1(MC, Ony, ZG) = 78 | Phase 2(BWL) = 83 | Phase 2.5(AQ40) = 88 | Phase 3(Naxx40) = 92 # Max iLVL Phase 1(MC, Ony, ZG) = 78 | Phase 2(BWL) = 83 | Phase 2.5(AQ40) = 88 | Phase 3(Naxx40) = 92
# TBC # TBC
# Max iLVL Tier 4 = 120 | Tier 5 = 133 | Tier 6 = 164 # Max iLVL Tier 4 = 120 | Tier 5 = 133 | Tier 6 = 164
# Max iLVL Phase 1(Kara, Gruul, Mag) = 125 | Phase 1.5(ZA) = 138 | Phase 2(SC, TK) = 141 | Phase 3(Hyjal, BT) = 156 | Phase 4(Sunwell) = 164 # Max iLVL Phase 1(Kara, Gruul, Mag) = 125 | Phase 2(SSC, TK, ZA) = 141 | Phase 3(Hyjal, BT) = 156 | Phase 4(Sunwell) = 164
# Wotlk # Wotlk
# Max iLVL Tier 7(10/25) = 200/213 | Tier 8(10/25) = 225/232 | Tier 9(10/25) = 232/245 | Tier 10(10/25/HC) = 251/264/290 # Max iLVL Tier 7(10/25) = 200/213 | Tier 8(10/25) = 225/232 | Tier 9(10/25) = 232/245 | Tier 10(10/25/HC) = 251/264/290
# Max iLVL Phase 1(Naxx) = 224 | Phase 2(Ulduar) = 245 | Phase 3(ToC) = 258 | Phase 4(ICC) = 290 # Max iLVL Phase 1(Naxx) = 224 | Phase 2(Ulduar) = 245 | Phase 3(ToC) = 258 | Phase 4(ICC) = 290
@@ -711,7 +745,8 @@ AiPlayerbot.RandomGearScoreLimit = 0
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.IncrementalGearInit = 1 AiPlayerbot.IncrementalGearInit = 1
# Set minimum level of bots that will enchant their equipment (if greater than RandomBotMaxlevel, bots will not enchant equipment) # Set minimum level of bots that will enchant and socket gems into their equipment with maintenance
# If greater than RandomBotMaxlevel, bots will not automatically enchant equipment or socket gems
# Default: 60 # Default: 60
AiPlayerbot.MinEnchantingBotLevel = 60 AiPlayerbot.MinEnchantingBotLevel = 60
@@ -863,13 +898,15 @@ AiPlayerbot.OpenGoSpell = 6477
# #
# Additional randombot strategies # Additional randombot strategies
# Strategies added here are applied to all randombots, in addition (or subtraction) to spec/role-based default strategies. These rules are processed after the defaults. # Strategies added here are applied to all randombots, in addition (or subtraction) to spec/role-based default strategies.
# These rules are processed after the defaults.
# Example: "+threat,-potions" # Example: "+threat,-potions"
AiPlayerbot.RandomBotCombatStrategies = "" AiPlayerbot.RandomBotCombatStrategies = ""
AiPlayerbot.RandomBotNonCombatStrategies = "" AiPlayerbot.RandomBotNonCombatStrategies = ""
# Additional altbot strategies # Additional altbot strategies
# Strategies added here are applied to all altbots, in addition (or subtraction) to spec/role-based default strategies. These rules are processed after the defaults. # Strategies added here are applied to all altbots, in addition (or subtraction) to spec/role-based default strategies.
# These rules are processed after the defaults.
AiPlayerbot.CombatStrategies = "" AiPlayerbot.CombatStrategies = ""
AiPlayerbot.NonCombatStrategies = "" AiPlayerbot.NonCombatStrategies = ""
@@ -1185,7 +1222,7 @@ AiPlayerbot.DeleteRandomBotArenaTeams = 0
AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951" AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951"
# PvP Restricted Areas (bots don't pvp) # PvP Restricted Areas (bots don't pvp)
AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754" AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973"
# Improve reaction speeds in battlegrounds and arenas (may cause lag) # Improve reaction speeds in battlegrounds and arenas (may cause lag)
AiPlayerbot.FastReactInBG = 1 AiPlayerbot.FastReactInBG = 1
@@ -1455,7 +1492,7 @@ AiPlayerbot.PremadeSpecLink.5.5.80 = 50332031003--005323241223112003102311351
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# DEATHKNIGHT # DEATH KNIGHT
# #
# #
@@ -1778,7 +1815,7 @@ AiPlayerbot.RandomClassSpecIndex.5.2 = 2
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# DEATHKNIGHT # DEATH KNIGHT
# #
# #

View File

@@ -0,0 +1,255 @@
DELETE FROM ai_playerbot_texts WHERE name IN (
'pet_usage_error',
'pet_no_pet_error',
'pet_stance_report',
'pet_no_target_error',
'pet_target_dead_error',
'pet_invalid_target_error',
'pet_pvp_prohibited_error',
'pet_attack_success',
'pet_attack_failed',
'pet_follow_success',
'pet_stay_success',
'pet_unknown_command_error',
'pet_stance_set_success',
'pet_type_pet',
'pet_type_guardian',
'pet_stance_aggressive',
'pet_stance_defensive',
'pet_stance_passive',
'pet_stance_unknown'
);
DELETE FROM ai_playerbot_texts_chance WHERE name IN (
'pet_usage_error',
'pet_no_pet_error',
'pet_stance_report',
'pet_no_target_error',
'pet_target_dead_error',
'pet_invalid_target_error',
'pet_pvp_prohibited_error',
'pet_attack_success',
'pet_attack_failed',
'pet_follow_success',
'pet_stay_success',
'pet_unknown_command_error',
'pet_stance_set_success',
'pet_type_pet',
'pet_type_guardian',
'pet_stance_aggressive',
'pet_stance_defensive',
'pet_stance_passive',
'pet_stance_unknown'
);
INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES
(1717, 'pet_usage_error', "Usage: pet <aggressive|defensive|passive|stance|attack|follow|stay>", 0, 0,
"사용법: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Utilisation: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Verwendung: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Использование: pet <aggressive|defensive|passive|stance|attack|follow|stay>"),
(1718, 'pet_no_pet_error', "You have no pet or guardian pet.", 0, 0,
"펫이나 수호자 펫이 없습니다.",
"Vous n'avez pas de familier ou gardien.",
"Du hast kein Tier oder Wächter.",
"你没有宠物或守护者宠物。",
"你沒有寵物或守護者寵物。",
"No tienes mascota o mascota guardián.",
"No tienes mascota o mascota guardián.",
"У вас нет питомца или защитника."),
(1719, 'pet_stance_report', "Current stance of %type \"%name\": %stance.", 0, 0,
"%type \"%name\"의 현재 태세: %stance.",
"Position actuelle du %type \"%name\": %stance.",
"Aktuelle Haltung des %type \"%name\": %stance.",
"%type \"%name\" 的当前姿态: %stance。",
"%type \"%name\" 的當前姿態: %stance。",
"Postura actual del %type \"%name\": %stance.",
"Postura actual del %type \"%name\": %stance.",
"Текущая позиция %type \"%name\": %stance."),
(1720, 'pet_no_target_error', "No valid target selected by master.", 0, 0,
"주인이 유효한 대상을 선택하지 않았습니다.",
"Aucune cible valide sélectionnée par le maître.",
"Kein gültiges Ziel vom Meister ausgewählt.",
"主人未选择有效目标。",
"主人未選擇有效目標。",
"No hay objetivo válido seleccionado por el maestro.",
"No hay objetivo válido seleccionado por el maestro.",
"Хозяин не выбрал действительную цель."),
(1721, 'pet_target_dead_error', "Target is not alive.", 0, 0,
"대상이 살아있지 않습니다.",
"La cible n'est pas vivante.",
"Das Ziel ist nicht am Leben.",
"目标未存活。",
"目標未存活。",
"El objetivo no está vivo.",
"El objetivo no está vivo.",
"Цель не жива."),
(1722, 'pet_invalid_target_error', "Target is not a valid attack target for the bot.", 0, 0,
"대상이 봇에게 유효한 공격 대상이 아닙니다.",
"La cible n'est pas une cible d'attaque valide pour le bot.",
"Das Ziel ist kein gültiges Angriffsziel für den Bot.",
"目标不是机器人的有效攻击目标。",
"目標不是機器人的有效攻擊目標。",
"El objetivo no es un objetivo de ataque válido para el bot.",
"El objetivo no es un objetivo de ataque válido para el bot.",
"Цель не является допустимой целью атаки для бота."),
(1723, 'pet_pvp_prohibited_error', "I cannot command my pet to attack players in PvP prohibited areas.", 0, 0,
"PvP 금지 지역에서는 펫에게 플레이어 공격 명령을 내릴 수 없습니다.",
"Je ne peux pas commander à mon familier d'attaquer des joueurs dans les zones où le PvP est interdit.",
"Ich kann meinem Tier nicht befehlen, Spieler in PvP-verbotenen Gebieten anzugreifen.",
"我不能命令我的宠物在禁止PvP的区域攻击玩家。",
"我不能命令我的寵物在禁止PvP的區域攻擊玩家。",
"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.",
"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.",
"Я не могу приказать своему питомцу атаковать игроков в зонах, где PvP запрещено."),
(1724, 'pet_attack_success', "Pet commanded to attack your target.", 0, 0,
"펫이 당신의 대상을 공격하도록 명령했습니다.",
"Le familier a reçu l'ordre d'attaquer votre cible.",
"Tier wurde befohlen, dein Ziel anzugreifen.",
"宠物已命令攻击你的目标。",
"寵物已命令攻擊你的目標。",
"Mascota ordenada a atacar tu objetivo.",
"Mascota ordenada a atacar tu objetivo.",
"Питомцу приказано атаковать вашу цель."),
(1725, 'pet_attack_failed', "Pet did not attack. (Already attacking or unable to attack target)", 0, 0,
"펫이 공격하지 않았습니다. (이미 공격 중이거나 대상 공격 불가)",
"Le familier n'a pas attaqué. (Attaque déjà en cours ou impossible d'attaquer la cible)",
"Tier hat nicht angegriffen. (Greift bereits an oder kann Ziel nicht angreifen)",
"宠物未攻击。(已在攻击或无法攻击目标)",
"寵物未攻擊。(已在攻擊或無法攻擊目標)",
"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)",
"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)",
"Питомец не атаковал. (Уже атакует или не может атаковать цель)"),
(1726, 'pet_follow_success', "Pet commanded to follow.", 0, 0,
"펫이 따라오도록 명령했습니다.",
"Le familier a reçu l'ordre de suivre.",
"Tier wurde befohlen zu folgen.",
"宠物已命令跟随。",
"寵物已命令跟隨。",
"Mascota ordenada a seguir.",
"Mascota ordenada a seguir.",
"Питомцу приказано следовать."),
(1727, 'pet_stay_success', "Pet commanded to stay.", 0, 0,
"펫이 머물도록 명령했습니다.",
"Le familier a reçu l'ordre de rester.",
"Tier wurde befohlen zu bleiben.",
"宠物已命令停留。",
"寵物已命令停留。",
"Mascota ordenada a quedarse.",
"Mascota ordenada a quedarse.",
"Питомцу приказано остаться."),
(1728, 'pet_unknown_command_error', "Unknown pet command: %param. Use: pet <aggressive|defensive|passive|stance|attack|follow|stay>", 0, 0,
"알 수 없는 펫 명령: %param. 사용법: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Commande de familier inconnue: %param. Utilisation: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Unbekannter Tierbefehl: %param. Verwendung: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"未知宠物命令: %param。用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"未知寵物命令: %param。用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Comando de mascota desconocido: %param. Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Comando de mascota desconocido: %param. Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Неизвестная команда питомца: %param. Использование: pet <aggressive|defensive|passive|stance|attack|follow|stay>"),
(1729, 'pet_stance_set_success', "Pet stance set to %stance.", 0, 0,
"펫 태세가 %stance(으)로 설정되었습니다.",
"Position du familier définie sur %stance.",
"Tierhaltung auf %stance gesetzt.",
"宠物姿态设置为 %stance。",
"寵物姿態設置為 %stance。",
"Postura de mascota establecida en %stance.",
"Postura de mascota establecida en %stance.",
"Позиция питомца установлена на %stance."),
(1730, 'pet_type_pet', "pet", 0, 0,
"",
"familier",
"Tier",
"宠物",
"寵物",
"mascota",
"mascota",
"питомец"),
(1731, 'pet_type_guardian', "guardian", 0, 0,
"수호자",
"gardien",
"Wächter",
"守护者",
"守護者",
"guardián",
"guardián",
"защитник"),
(1732, 'pet_stance_aggressive', "aggressive", 0, 0,
"공격적",
"agressif",
"aggressiv",
"进攻",
"進攻",
"agresivo",
"agresivo",
"агрессивная"),
(1733, 'pet_stance_defensive', "defensive", 0, 0,
"방어적",
"défensif",
"defensiv",
"防御",
"防禦",
"defensivo",
"defensivo",
"защитная"),
(1734, 'pet_stance_passive', "passive", 0, 0,
"수동적",
"passif",
"passiv",
"被动",
"被動",
"pasivo",
"pasivo",
"пассивная"),
(1735, 'pet_stance_unknown', "unknown", 0, 0,
"알 수 없음",
"inconnu",
"unbekannt",
"未知",
"未知",
"desconocido",
"desconocido",
"неизвестная");
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES
('pet_usage_error', 100),
('pet_no_pet_error', 100),
('pet_stance_report', 100),
('pet_no_target_error', 100),
('pet_target_dead_error', 100),
('pet_invalid_target_error', 100),
('pet_pvp_prohibited_error', 100),
('pet_attack_success', 100),
('pet_attack_failed', 100),
('pet_follow_success', 100),
('pet_stay_success', 100),
('pet_unknown_command_error', 100),
('pet_stance_set_success', 100),
('pet_type_pet', 100),
('pet_type_guardian', 100),
('pet_stance_aggressive', 100),
('pet_stance_defensive', 100),
('pet_stance_passive', 100),
('pet_stance_unknown', 100);

View File

@@ -0,0 +1,15 @@
DELETE FROM ai_playerbot_texts WHERE name IN ('no_fishing_pole_error');
DELETE FROM ai_playerbot_texts_chance WHERE name IN ('no_fishing_pole_error');
INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES
(1736, 'no_fishing_pole_error', "I don't have a Fishing Pole", 0, 0,
"낚싯대가 없습니다",
"Je nai pas de canne à pêche",
"Ich habe keine Angelrute",
"我沒有釣魚竿",
"我没有钓鱼竿",
"No tengo una caña de pescar",
"No tengo una caña de pescar",
"У меня нет удочки");
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('no_fishing_pole_error', 100);

View File

@@ -46,7 +46,6 @@
#include "OutfitAction.h" #include "OutfitAction.h"
#include "PositionAction.h" #include "PositionAction.h"
#include "DropQuestAction.h" #include "DropQuestAction.h"
#include "RaidNaxxActions.h"
#include "RandomBotUpdateAction.h" #include "RandomBotUpdateAction.h"
#include "ReachTargetActions.h" #include "ReachTargetActions.h"
#include "ReleaseSpiritAction.h" #include "ReleaseSpiritAction.h"
@@ -64,6 +63,7 @@
#include "WorldBuffAction.h" #include "WorldBuffAction.h"
#include "XpGainAction.h" #include "XpGainAction.h"
#include "NewRpgAction.h" #include "NewRpgAction.h"
#include "FishingAction.h"
#include "CancelChannelAction.h" #include "CancelChannelAction.h"
class PlayerbotAI; class PlayerbotAI;
@@ -121,7 +121,7 @@ public:
creators["shoot"] = &ActionContext::shoot; creators["shoot"] = &ActionContext::shoot;
creators["follow"] = &ActionContext::follow; creators["follow"] = &ActionContext::follow;
creators["move from group"] = &ActionContext::move_from_group; creators["move from group"] = &ActionContext::move_from_group;
creators["flee to master"] = &ActionContext::flee_to_master; creators["flee to group leader"] = &ActionContext::flee_to_group_leader;
creators["runaway"] = &ActionContext::runaway; creators["runaway"] = &ActionContext::runaway;
creators["stay"] = &ActionContext::stay; creators["stay"] = &ActionContext::stay;
creators["sit"] = &ActionContext::sit; creators["sit"] = &ActionContext::sit;
@@ -191,6 +191,11 @@ public:
creators["buy tabard"] = &ActionContext::buy_tabard; creators["buy tabard"] = &ActionContext::buy_tabard;
creators["guild manage nearby"] = &ActionContext::guild_manage_nearby; creators["guild manage nearby"] = &ActionContext::guild_manage_nearby;
creators["clean quest log"] = &ActionContext::clean_quest_log; creators["clean quest log"] = &ActionContext::clean_quest_log;
creators["move near water"] = &ActionContext::move_near_water;
creators["go fishing"] = &ActionContext::go_fishing;
creators["use fishing bobber"] = &ActionContext::use_fishing_bobber;
creators["end master fishing"] = &ActionContext::end_master_fishing;
creators["remove bobber strategy"] = &ActionContext::remove_bobber_strategy;
creators["roll"] = &ActionContext::roll_action; creators["roll"] = &ActionContext::roll_action;
creators["cancel channel"] = &ActionContext::cancel_channel; creators["cancel channel"] = &ActionContext::cancel_channel;
@@ -318,7 +323,7 @@ private:
static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); } static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); }
static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); } static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); }
static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); } static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); }
static Action* flee_to_master(PlayerbotAI* botAI) { return new FleeToMasterAction(botAI); } static Action* flee_to_group_leader(PlayerbotAI* botAI) { return new FleeToGroupLeaderAction(botAI); }
static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); } static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); }
static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); } static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); }
static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); } static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); }
@@ -380,6 +385,11 @@ private:
static Action* buy_tabard(PlayerbotAI* botAI) { return new BuyTabardAction(botAI); } static Action* buy_tabard(PlayerbotAI* botAI) { return new BuyTabardAction(botAI); }
static Action* guild_manage_nearby(PlayerbotAI* botAI) { return new GuildManageNearbyAction(botAI); } static Action* guild_manage_nearby(PlayerbotAI* botAI) { return new GuildManageNearbyAction(botAI); }
static Action* clean_quest_log(PlayerbotAI* botAI) { return new CleanQuestLogAction(botAI); } static Action* clean_quest_log(PlayerbotAI* botAI) { return new CleanQuestLogAction(botAI); }
static Action* move_near_water(PlayerbotAI* botAI) { return new MoveNearWaterAction(botAI); }
static Action* go_fishing(PlayerbotAI* botAI) { return new FishingAction(botAI);}
static Action* use_fishing_bobber(PlayerbotAI* botAI) { return new UseBobberAction(botAI);}
static Action* end_master_fishing(PlayerbotAI* botAI) { return new EndMasterFishingAction(botAI); }
static Action* remove_bobber_strategy(PlayerbotAI* botAI) { return new RemoveBobberStrategyAction(botAI); }
static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); }
// BG Tactics // BG Tactics

View File

@@ -49,7 +49,7 @@ bool AcceptInvitationAction::Execute(Event event)
if (sRandomPlayerbotMgr->IsRandomBot(bot)) if (sRandomPlayerbotMgr->IsRandomBot(bot))
botAI->SetMaster(inviter); botAI->SetMaster(inviter);
// else // else
// sPlayerbotDbStore->Save(botAI); // sPlayerbotRepository->Save(botAI);
botAI->ResetStrategies(); botAI->ResetStrategies();
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
@@ -59,7 +59,7 @@ bool AcceptInvitationAction::Execute(Event event)
if (sPlayerbotAIConfig->summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig->sightDistance) if (sPlayerbotAIConfig->summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig->sightDistance)
{ {
Teleport(inviter, bot); Teleport(inviter, bot, true);
} }
return true; return true;
} }

View File

@@ -23,7 +23,5 @@ bool AcceptResurrectAction::Execute(Event event)
packet << uint8(1); // accept packet << uint8(1); // accept
bot->GetSession()->HandleResurrectResponseOpcode(packet); // queue the packet to get around race condition bot->GetSession()->HandleResurrectResponseOpcode(packet); // queue the packet to get around race condition
botAI->ChangeEngine(BOT_STATE_NON_COMBAT);
return true; return true;
} }

View File

@@ -15,20 +15,19 @@
#include "SharedDefines.h" #include "SharedDefines.h"
#include "Unit.h" #include "Unit.h"
bool AttackAction::Execute(Event event) bool AttackAction::Execute(Event /*event*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
if (!target->IsInWorld()) if (!target->IsInWorld())
{
return false; return false;
}
return Attack(target); return Attack(target);
} }
bool AttackMyTargetAction::Execute(Event event) bool AttackMyTargetAction::Execute(Event /*event*/)
{ {
Player* master = GetMaster(); Player* master = GetMaster();
if (!master) if (!master)
@@ -51,7 +50,7 @@ bool AttackMyTargetAction::Execute(Event event)
return result; return result;
} }
bool AttackAction::Attack(Unit* target, bool with_pet /*true*/) bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
{ {
Unit* oldTarget = context->GetValue<Unit*>("current target")->Get(); Unit* oldTarget = context->GetValue<Unit*>("current target")->Get();
bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot);
@@ -81,12 +80,15 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is no longer in the world."); botAI->TellError(std::string(target->GetName()) + " is no longer in the world.");
return false; return false;
} }
if ((sPlayerbotAIConfig->IsInPvpProhibitedZone(bot->GetZoneId()) || // Check if bot OR target is in prohibited zone/area (skip for duels)
sPlayerbotAIConfig->IsInPvpProhibitedArea(bot->GetAreaId())) if ((target->IsPlayer() || target->IsPet()) &&
&& (target->IsPlayer() || target->IsPet())) (!bot->duel || bot->duel->Opponent != target) &&
(sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) ||
sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
{ {
if (verbose) if (verbose)
botAI->TellError("I cannot attack other players in PvP prohibited areas."); botAI->TellError("I cannot attack other players in PvP prohibited areas.");
@@ -98,6 +100,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is friendly to me."); botAI->TellError(std::string(target->GetName()) + " is friendly to me.");
return false; return false;
} }
@@ -105,6 +108,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is dead."); botAI->TellError(std::string(target->GetName()) + " is dead.");
return false; return false;
} }
@@ -112,6 +116,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is not in my sight."); botAI->TellError(std::string(target->GetName()) + " is not in my sight.");
return false; return false;
} }
@@ -119,6 +124,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError("I am already attacking " + std::string(target->GetName()) + "."); botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
return false; return false;
} }
@@ -153,10 +159,9 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
bot->StopMoving(); bot->StopMoving();
} }
if (IsMovingAllowed() && !bot->HasInArc(CAST_ANGLE_IN_FRONT, target)) if (botAI->CanMove() && !bot->HasInArc(CAST_ANGLE_IN_FRONT, target))
{
sServerFacade->SetFacingTo(bot, target); sServerFacade->SetFacingTo(bot, target);
}
botAI->ChangeEngine(BOT_STATE_COMBAT); botAI->ChangeEngine(BOT_STATE_COMBAT);
bot->Attack(target, shouldMelee); bot->Attack(target, shouldMelee);
@@ -186,4 +191,4 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
bool AttackDuelOpponentAction::isUseful() { return AI_VALUE(Unit*, "duel target"); } bool AttackDuelOpponentAction::isUseful() { return AI_VALUE(Unit*, "duel target"); }
bool AttackDuelOpponentAction::Execute(Event event) { return Attack(AI_VALUE(Unit*, "duel target")); } bool AttackDuelOpponentAction::Execute(Event /*event*/) { return Attack(AI_VALUE(Unit*, "duel target")); }

View File

@@ -75,36 +75,79 @@ void AutoMaintenanceOnLevelupAction::LearnSpells(std::ostringstream* out)
void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out) void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out)
{ {
PlayerbotFactory factory(bot, bot->GetLevel()); PlayerbotFactory factory(bot, bot->GetLevel());
factory.InitSkills();
factory.InitClassSpells(); factory.InitClassSpells();
factory.InitAvailableSpells(); factory.InitAvailableSpells();
factory.InitSkills();
factory.InitPet(); factory.InitPet();
} }
void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out) void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
{ {
// CreatureTemplate const* co = sCreatureStorage.LookupEntry<CreatureTemplate>(id);
ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates();
for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i)
{ {
//uint32 questId = i->first; //not used, line marked for removal.
Quest const* quest = i->second; Quest const* quest = i->second;
if (!quest->GetRequiredClasses() || quest->IsRepeatable() || quest->GetMinLevel() < 10) // only process class-specific quests to learn class-related spells, cuz
// we don't want all these bunch of entries to be handled!
if (!quest->GetRequiredClasses())
continue; continue;
if (!bot->SatisfyQuestClass(quest, false) || quest->GetMinLevel() > bot->GetLevel() || // skip quests that are repeatable, too low level, or above bots' level
!bot->SatisfyQuestRace(quest, false)) if (quest->IsRepeatable() || quest->GetMinLevel() < 10 || quest->GetMinLevel() > bot->GetLevel())
continue; continue;
if (quest->GetRewSpellCast() > 0) // skip if bot doesnt satisfy class, race, or skill requirements
if (!bot->SatisfyQuestClass(quest, false) || !bot->SatisfyQuestRace(quest, false) ||
!bot->SatisfyQuestSkill(quest, false))
continue;
// use the same logic and impl from Player::learnQuestRewardedSpells
int32 spellId = quest->GetRewSpellCast();
if (!spellId)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
// xinef: find effect with learn spell and check if we have this spell
bool found = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{ {
LearnSpell(quest->GetRewSpellCast(), out); if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && spellInfo->Effects[i].TriggerSpell &&
!bot->HasSpell(spellInfo->Effects[i].TriggerSpell))
{
// pusywizard: don't re-add profession specialties!
if (SpellInfo const* triggeredInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell))
if (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL)
break; // pussywizard: break and not cast the spell (found is false)
found = true;
break;
}
} }
else if (quest->GetRewSpell() > 0)
// xinef: we know the spell, continue
if (!found)
continue;
bot->CastSpell(bot, spellId, true);
// Check if RewardDisplaySpell is set to output the proper spell learned
// after processing quests. Output the original RewardSpell otherwise.
uint32 rewSpellId = quest->GetRewSpell();
if (rewSpellId)
{ {
LearnSpell(quest->GetRewSpell(), out); if (SpellInfo const* rewSpellInfo = sSpellMgr->GetSpellInfo(rewSpellId))
{
*out << FormatSpell(rewSpellInfo) << ", ";
continue;
}
} }
*out << FormatSpell(spellInfo) << ", ";
} }
} }
@@ -121,39 +164,6 @@ std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* s
return out.str(); return out.str();
} }
void AutoMaintenanceOnLevelupAction::LearnSpell(uint32 spellId, std::ostringstream* out)
{
SpellInfo const* proto = sSpellMgr->GetSpellInfo(spellId);
if (!proto)
return;
bool learned = false;
for (uint8 j = 0; j < 3; ++j)
{
if (proto->Effects[j].Effect == SPELL_EFFECT_LEARN_SPELL)
{
uint32 learnedSpell = proto->Effects[j].TriggerSpell;
if (!bot->HasSpell(learnedSpell))
{
bot->learnSpell(learnedSpell);
*out << FormatSpell(sSpellMgr->GetSpellInfo(learnedSpell)) << ", ";
}
learned = true;
}
}
if (!learned)
{
if (!bot->HasSpell(spellId))
{
bot->learnSpell(spellId);
*out << FormatSpell(proto) << ", ";
}
}
}
void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip() void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
{ {
if (!sPlayerbotAIConfig->autoUpgradeEquip || !sRandomPlayerbotMgr->IsRandomBot(bot)) if (!sPlayerbotAIConfig->autoUpgradeEquip || !sRandomPlayerbotMgr->IsRandomBot(bot))

View File

@@ -28,7 +28,6 @@ protected:
void LearnSpells(std::ostringstream* out); void LearnSpells(std::ostringstream* out);
void LearnTrainerSpells(std::ostringstream* out); void LearnTrainerSpells(std::ostringstream* out);
void LearnQuestSpells(std::ostringstream* out); void LearnQuestSpells(std::ostringstream* out);
void LearnSpell(uint32 spellId, std::ostringstream* out);
std::string const FormatSpell(SpellInfo const* sInfo); std::string const FormatSpell(SpellInfo const* sInfo);
}; };

View File

@@ -224,42 +224,36 @@ bool BuyAction::Execute(Event event)
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto) bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)
{ {
uint32 oldCount = AI_VALUE2(uint32, "item count", proto->Name1); if (!tItems || !proto)
if (!tItems)
return false; return false;
uint32 itemId = proto->ItemId; uint32 itemId = proto->ItemId;
for (uint32 slot = 0; slot < tItems->GetItemCount(); slot++) uint32 oldCount = bot->GetItemCount(itemId, false);
for (uint32 slot = 0; slot < tItems->GetItemCount(); ++slot)
{ {
if (tItems->GetItem(slot)->item == itemId) if (tItems->GetItem(slot)->item != itemId)
continue;
uint32 botMoney = bot->GetMoney();
if (botAI->HasCheat(BotCheatMask::gold))
bot->SetMoney(10000000);
bot->BuyItemFromVendorSlot(vendorguid, slot, itemId, 1, NULL_BAG, NULL_SLOT);
if (botAI->HasCheat(BotCheatMask::gold))
bot->SetMoney(botMoney);
uint32 newCount = bot->GetItemCount(itemId, false);
if (newCount > oldCount)
{ {
uint32 botMoney = bot->GetMoney(); std::ostringstream out;
if (botAI->HasCheat(BotCheatMask::gold)) out << "Buying " << ChatHelper::FormatItem(proto);
{ botAI->TellMaster(out.str());
bot->SetMoney(10000000); return true;
}
bot->BuyItemFromVendorSlot(vendorguid, slot, itemId, 1, NULL_BAG, NULL_SLOT);
if (botAI->HasCheat(BotCheatMask::gold))
{
bot->SetMoney(botMoney);
}
if (oldCount <
AI_VALUE2(
uint32, "item count",
proto->Name1)) // BuyItem Always returns false (unless unique) so we have to check the item counts.
{
std::ostringstream out;
out << "Buying " << ChatHelper::FormatItem(proto);
botAI->TellMaster(out.str());
return true;
}
return false;
} }
return false;
} }
return false; return false;

View File

@@ -6,7 +6,7 @@
#include "ChangeStrategyAction.h" #include "ChangeStrategyAction.h"
#include "Event.h" #include "Event.h"
#include "PlayerbotDbStore.h" #include "PlayerbotRepository.h"
#include "Playerbots.h" #include "Playerbots.h"
bool ChangeCombatStrategyAction::Execute(Event event) bool ChangeCombatStrategyAction::Execute(Event event)
@@ -24,7 +24,7 @@ bool ChangeCombatStrategyAction::Execute(Event event)
case '+': case '+':
case '-': case '-':
case '~': case '~':
sPlayerbotDbStore->Save(botAI); sPlayerbotRepository->Save(botAI);
break; break;
case '?': case '?':
break; break;
@@ -62,7 +62,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
case '+': case '+':
case '-': case '-':
case '~': case '~':
sPlayerbotDbStore->Save(botAI); sPlayerbotRepository->Save(botAI);
break; break;
case '?': case '?':
break; break;

View File

@@ -241,20 +241,6 @@ bool MaxDpsChatShortcutAction::Execute(Event event)
return true; return true;
} }
bool NaxxChatShortcutAction::Execute(Event event)
{
Player* master = GetMaster();
if (!master)
return false;
botAI->Reset();
botAI->ChangeStrategy("+naxx", BOT_STATE_NON_COMBAT);
botAI->ChangeStrategy("+naxx", BOT_STATE_COMBAT);
botAI->TellMasterNoFacing("Add Naxx Strategies!");
// bot->Say("Add Naxx Strategies!", LANG_UNIVERSAL);
return true;
}
bool BwlChatShortcutAction::Execute(Event event) bool BwlChatShortcutAction::Execute(Event event)
{ {
Player* master = GetMaster(); Player* master = GetMaster();

View File

@@ -85,13 +85,6 @@ public:
bool Execute(Event event) override; bool Execute(Event event) override;
}; };
class NaxxChatShortcutAction : public Action
{
public:
NaxxChatShortcutAction(PlayerbotAI* ai) : Action(ai, "naxx chat shortcut") {}
virtual bool Execute(Event event);
};
class BwlChatShortcutAction : public Action class BwlChatShortcutAction : public Action
{ {
public: public:

View File

@@ -78,20 +78,17 @@ float ChooseRpgTargetAction::getMaxRelevance(GuidPosition guidP)
if (!trigger->IsActive()) if (!trigger->IsActive())
continue; continue;
NextAction** nextActions = triggerNode->getHandlers(); std::vector<NextAction> nextActions = triggerNode->getHandlers();
bool isRpg = false; bool isRpg = false;
for (int32 i = 0; i < NextAction::size(nextActions); i++) for (NextAction nextAction : nextActions)
{ {
NextAction* nextAction = nextActions[i]; Action* action = botAI->GetAiObjectContext()->GetAction(nextAction.getName());
Action* action = botAI->GetAiObjectContext()->GetAction(nextAction->getName());
if (dynamic_cast<RpgEnabled*>(action)) if (dynamic_cast<RpgEnabled*>(action))
isRpg = true; isRpg = true;
} }
NextAction::destroy(nextActions);
if (isRpg) if (isRpg)
{ {
@@ -311,7 +308,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldObject* target)
bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos)
{ {
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
Player* gmaster = botAI->GetGroupMaster(); Player* groupLeader = botAI->GetGroupLeader();
Player* realMaster = botAI->GetMaster(); Player* realMaster = botAI->GetMaster();
AiObjectContext* context = botAI->GetAiObjectContext(); AiObjectContext* context = botAI->GetAiObjectContext();
@@ -327,30 +324,30 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos)
return false; return false;
} }
if (!gmaster || bot == gmaster) if (!groupLeader || bot == groupLeader)
return true; return true;
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
return true; return true;
if (bot->GetDistance(gmaster) > sPlayerbotAIConfig->rpgDistance * 2) if (bot->GetDistance(groupLeader) > sPlayerbotAIConfig->rpgDistance * 2)
return false; return false;
Formation* formation = AI_VALUE(Formation*, "formation"); Formation* formation = AI_VALUE(Formation*, "formation");
float distance = gmaster->GetDistance2d(pos.getX(), pos.getY()); float distance = groupLeader->GetDistance2d(pos.getX(), pos.getY());
if (!botAI->HasActivePlayerMaster() && distance < 50.0f) if (!botAI->HasActivePlayerMaster() && distance < 50.0f)
{ {
Player* player = gmaster; Player* player = groupLeader;
if (gmaster && !gmaster->isMoving() || if (groupLeader && !groupLeader->isMoving() ||
PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance) PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance)
return true; return true;
} }
if ((inDungeon || !gmaster->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == gmaster && distance > 5.0f) if ((inDungeon || !groupLeader->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == groupLeader && distance > 5.0f)
return false; return false;
if (!gmaster->isMoving() && distance < 25.0f) if (!groupLeader->isMoving() && distance < 25.0f)
return true; return true;
if (distance < formation->GetMaxDistance()) if (distance < formation->GetMaxDistance())

View File

@@ -10,6 +10,7 @@
#include "LootObjectStack.h" #include "LootObjectStack.h"
#include "NewRpgStrategy.h" #include "NewRpgStrategy.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RtiTargetValue.h"
#include "PossibleRpgTargetsValue.h" #include "PossibleRpgTargetsValue.h"
#include "PvpTriggers.h" #include "PvpTriggers.h"
#include "ServerFacade.h" #include "ServerFacade.h"
@@ -87,9 +88,7 @@ bool DropTargetAction::Execute(Event event)
{ {
Spell const* spell = bot->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL); // Get the current spell being cast by the bot Spell const* spell = bot->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL); // Get the current spell being cast by the bot
if (spell && spell->m_spellInfo->Id == 75) //Check spell is not nullptr before accessing m_spellInfo if (spell && spell->m_spellInfo->Id == 75) //Check spell is not nullptr before accessing m_spellInfo
{
bot->InterruptSpell(CURRENT_AUTOREPEAT_SPELL); // Interrupt Auto Shot bot->InterruptSpell(CURRENT_AUTOREPEAT_SPELL); // Interrupt Auto Shot
}
} }
bot->AttackStop(); bot->AttackStop();
@@ -142,6 +141,23 @@ bool AttackRtiTargetAction::Execute(Event event)
{ {
Unit* rtiTarget = AI_VALUE(Unit*, "rti target"); Unit* rtiTarget = AI_VALUE(Unit*, "rti target");
// Fallback: if the "rti target" value did not resolve a valid unit yet,
// try to resolve the raid icon directly from the group.
if (!rtiTarget)
{
if (Group* group = bot->GetGroup())
{
std::string const rti = AI_VALUE(std::string, "rti");
int32 const index = RtiTargetValue::GetRtiIndex(rti);
if (index >= 0)
{
ObjectGuid const guid = group->GetTargetIcon(index);
if (!guid.IsEmpty())
rtiTarget = botAI->GetUnit(guid);
}
}
}
if (rtiTarget && rtiTarget->IsInWorld() && rtiTarget->GetMapId() == bot->GetMapId()) if (rtiTarget && rtiTarget->IsInWorld() && rtiTarget->GetMapId() == bot->GetMapId())
{ {
botAI->GetAiObjectContext()->GetValue<GuidVector>("prioritized targets")->Set({rtiTarget->GetGUID()}); botAI->GetAiObjectContext()->GetValue<GuidVector>("prioritized targets")->Set({rtiTarget->GetGUID()});
@@ -153,9 +169,7 @@ bool AttackRtiTargetAction::Execute(Event event)
} }
} }
else else
{
botAI->TellError("I dont see my rti attack target"); botAI->TellError("I dont see my rti attack target");
}
return false; return false;
} }

View File

@@ -180,7 +180,7 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge
void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget) void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget)
{ {
// Tell the master where we are going. // Tell the master where we are going.
if (!bot->GetGroup() || (botAI->GetGroupMaster() == bot)) if (!bot->GetGroup() || (botAI->GetGroupLeader() == bot))
ReportTravelTarget(newTarget, oldTarget); ReportTravelTarget(newTarget, oldTarget);
// If we are heading to a creature/npc clear it from the ignore list. // If we are heading to a creature/npc clear it from the ignore list.

View File

@@ -344,6 +344,27 @@ bool EquipUpgradesAction::Execute(Event event)
return false; return false;
} }
if (event.GetSource() == "item push result")
{
WorldPacket p(event.getPacket());
p.rpos(0);
ObjectGuid playerGuid;
uint32 received, created, sendChatMessage, itemSlot, itemId;
uint8 bagSlot;
p >> playerGuid;
p >> received;
p >> created;
p >> sendChatMessage;
p >> bagSlot;
p >> itemSlot;
p >> itemId;
ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId);
if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT)
return false;
}
CollectItemsVisitor visitor; CollectItemsVisitor visitor;
IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS);

View File

@@ -0,0 +1,510 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "FishingAction.h"
#include "FishValues.h"
#include "Event.h"
#include "GridNotifiers.h"
#include "GridNotifiersImpl.h"
#include "ItemPackets.h"
#include "LastMovementValue.h"
#include "Map.h"
#include "MovementActions.h"
#include "Object.h"
#include "PlayerbotAI.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Position.h"
uint32 const FISHING_SPELL = 7620;
uint32 const FISHING_POLE = 6256;
uint32 const FISHING_BOBBER = 35591;
float const MIN_DISTANCE_TO_WATER = 10.0f; // Minimum spell distance
float const MAX_DISTANCE_TO_WATER = 20.0f; // Maximum spell distance
float const HEIGHT_ABOVE_WATER_TOLERANCE = 1.0f; // Can stand in up to 1 unit of water and still fish.
float const SEARCH_INCREMENT = 2.5f;
float const HEIGHT_SEARCH_BUFFER = 10.0f; // Height buffer to prevent potentially missing the model the bot is standing on.
float const SEARCH_LAND_BUFFER = 0.5f;
uint32 const FISHING_LOCATION_TIMEOUT = 180000; //Three minutes
static bool IsFishingPole(Item* const item)
{
if (!item)
return false;
const ItemTemplate* proto = item->GetTemplate();
return proto && proto->Class == ITEM_CLASS_WEAPON &&
proto->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE;
}
float HasFishableWaterOrLand(float x, float y, float z, Map* map, uint32 phaseMask, bool checkForLand=false)
{
if (!map)
return INVALID_HEIGHT;
LiquidData const& liq = map->GetLiquidData(phaseMask, x, y, z+HEIGHT_ABOVE_WATER_TOLERANCE, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS);
float ground = map->GetHeight(phaseMask, x, y, z + HEIGHT_SEARCH_BUFFER, true);
if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER)
{
if (checkForLand)
return ground;
return INVALID_HEIGHT;
}
if (checkForLand)
{
if (ground > liq.Level - HEIGHT_ABOVE_WATER_TOLERANCE)
return ground;
return INVALID_HEIGHT;
}
if (liq.Level + HEIGHT_ABOVE_WATER_TOLERANCE > ground)
{
if (abs(liq.DepthLevel) < 0.5f) // too shallow to fish in.
return INVALID_HEIGHT;
return liq.Level;
}
return INVALID_HEIGHT;
}
bool HasLosToWater(Player* bot, float wx, float wy, float waterZ)
{
float z = bot->GetCollisionHeight() + bot->GetPositionZ();
return bot->GetMap()->isInLineOfSight(
bot->GetPositionX(), bot->GetPositionY(), z,
wx, wy, waterZ,
bot->GetPhaseMask(),
LINEOFSIGHT_ALL_CHECKS,
VMAP::ModelIgnoreFlags::Nothing);
}
WorldPosition FindLandFromPosition(PlayerbotAI* botAI, float startDistance, float endDistance, float increment, float orientation, WorldPosition targetPos, float fishingSearchWindow, bool checkLOS = true)
{
Player* bot = botAI->GetBot();
Map* map = bot->GetMap();
uint32 phaseMask = bot->GetPhaseMask();
Player* master = botAI->GetMaster();
float targetX = targetPos.GetPositionX();
float targetY = targetPos.GetPositionY();
float targetZ = targetPos.GetPositionZ();
for (float dist = startDistance; dist <= endDistance; dist += increment)
{
//step backwards from position to bot to find edge of shore.
float checkX = targetX - dist * cos(orientation);
float checkY = targetY - dist * sin(orientation);
float groundZ = map->GetHeight(phaseMask, checkX, checkY, targetZ + HEIGHT_SEARCH_BUFFER, true);
if (groundZ == INVALID_HEIGHT)
continue;
LiquidData const& liq = map->GetLiquidData(phaseMask, checkX, checkY, targetZ, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS);
if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER || groundZ > liq.DepthLevel + HEIGHT_ABOVE_WATER_TOLERANCE)
{
if (checkLOS)
{
bool hasLOS = map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
if (!hasLOS)
continue;
}
// Add a distance check for the position to prevent the bot from moving out of range to the master.
if (master && botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER)
continue;
return WorldPosition(bot->GetMapId(), checkX, checkY, groundZ);
}
}
return WorldPosition();
}
WorldPosition FindLandRadialFromPosition (PlayerbotAI* botAI, WorldPosition targetPos, float startDistance, float endDistance, float increment, float fishingSearchWindow, int angles = 16)
{
Player* bot = botAI->GetBot();
const int numDirections = angles;
std::vector<WorldPosition> boundaryPoints;
Player* master = botAI->GetMaster();
if (!master)
return WorldPosition();
Map* map = bot->GetMap();
uint32 phaseMask = bot->GetPhaseMask();
float targetX = targetPos.GetPositionX();
float targetY = targetPos.GetPositionY();
float targetZ = targetPos.GetPositionZ();
for (float dist = startDistance; dist <= endDistance; dist += increment)
{
for (int i = 0; i < numDirections; ++i)
{
float angle = (2.0f * M_PI * i) / numDirections;
float checkX = targetX - cos(angle) * dist;
float checkY = targetY - sin(angle) * dist;
float groundZ = HasFishableWaterOrLand(checkX, checkY, targetZ, map, phaseMask, true);
if (groundZ == INVALID_HEIGHT)
continue;
if (map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER)
continue;
boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, groundZ));
}
if (!boundaryPoints.empty())
break;
}
if (boundaryPoints.empty())
return WorldPosition();
if (boundaryPoints.size() == 1)
return boundaryPoints[0];
float minDistance = FLT_MAX;
WorldLocation closestPoint = WorldPosition();
for (auto const& pos : boundaryPoints)
{
float distance = bot->GetExactDist2d(&pos);
if (distance < minDistance)
{
minDistance = distance;
closestPoint = pos;
}
}
return closestPoint;
}
WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS, int numDirections)
{
std::vector<WorldPosition> boundaryPoints;
float dist = minDistance;
while (dist <= maxDistance)
{
for (int i = 0; i < numDirections; ++i)
{
float angle = (2.0f * M_PI * i) / numDirections;
float checkX = x + cos(angle) * dist;
float checkY = y + sin(angle) * dist;
float waterZ = HasFishableWaterOrLand(checkX, checkY, z, map, phaseMask);
if (waterZ == INVALID_HEIGHT)
continue;
if (checkLOS && !HasLosToWater(bot, checkX, checkY, waterZ))
continue;
boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, waterZ));
}
if (!boundaryPoints.empty())
break;
dist += increment;
}
if (boundaryPoints.empty())
return WorldPosition();
if (boundaryPoints.size() == 1)
return boundaryPoints[0];
// return the central point in the identified positions in to try to be perpendicular to the shore.
return boundaryPoints[boundaryPoints.size() / 2];
}
WorldPosition FindFishingHole(PlayerbotAI* botAI)
{
Player* player = botAI->GetBot();
GuidVector gos = PAI_VALUE(GuidVector, "nearest game objects no los");
GameObject* nearestFishingHole = nullptr;
float minDist = std::numeric_limits<float>::max();
for (auto const& guid : gos)
{
GameObject* go = botAI->GetGameObject(guid);
if (!go)
continue;
if (go->GetGoType() == GAMEOBJECT_TYPE_FISHINGHOLE)
{
float dist = player->GetDistance2d(go);
if (dist < minDist)
{
minDist = dist;
nearestFishingHole = go;
}
}
}
if (nearestFishingHole)
return WorldPosition(nearestFishingHole->GetMapId(), nearestFishingHole->GetPositionX(), nearestFishingHole->GetPositionY(), nearestFishingHole->GetPositionZ());
return WorldPosition();
}
bool MoveNearWaterAction::Execute(Event event)
{
WorldPosition landSpot = AI_VALUE(WorldPosition, "fishing spot");
if (landSpot.IsValid())
return MoveTo(landSpot.GetMapId(), landSpot.GetPositionX(), landSpot.GetPositionY(), landSpot.GetPositionZ());
return false;
}
bool MoveNearWaterAction::isUseful()
{
if (!AI_VALUE(bool, "can fish"))
return false;
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
return !pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) ||
bot->GetExactDist(&pos) < 0.1f;
}
bool MoveNearWaterAction::isPossible()
{
Player* master = botAI->GetMaster();
float fishingSearchWindow;
if (master)
fishingSearchWindow = sPlayerbotAIConfig->fishingDistanceFromMaster;
else
fishingSearchWindow = sPlayerbotAIConfig->fishingDistance;
WorldPosition fishingHole = FindFishingHole(botAI);
if (fishingHole.IsValid())
{
float distance = bot->GetExactDist2d(&fishingHole);
bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ());
// Water spot is in range, and we have LOS to it. Set bot position to fishing spot and do not move
if (distance >= MIN_DISTANCE_TO_WATER &&
distance <= MAX_DISTANCE_TO_WATER && hasLOS)
{
SET_AI_VALUE(WorldPosition, "fishing spot", WorldPosition(WorldPosition(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())));
return false;
}
// Water spot is out of range, lets look for a spot to move to for the fishing hole.
if (distance > MAX_DISTANCE_TO_WATER || distance < MIN_DISTANCE_TO_WATER)
{
float angle = bot->GetAngle(fishingHole.GetPositionX(), fishingHole.GetPositionY());
WorldPosition landSpot = FindLandRadialFromPosition(botAI, fishingHole, MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, fishingSearchWindow, 32);
if (landSpot.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot", landSpot);
return true;
}
}
}
// Can the bot fish from current position?
WorldPosition waterAtCurrentPos =
FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMap(),
bot->GetPhaseMask(), MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, true);
if (waterAtCurrentPos.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot",
WorldPosition(WorldPosition(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ())));
return false;
}
// Lets find some water where we can fish.
WorldPosition water = FindWaterRadial(
bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
bot->GetMap(), bot->GetPhaseMask(),
MIN_DISTANCE_TO_WATER,
fishingSearchWindow + MAX_DISTANCE_TO_WATER,
SEARCH_INCREMENT, false);
if (!water.IsValid())
return false;
bool hasLOS = bot->IsWithinLOS(water.GetPositionX(), water.GetPositionY(), water.GetPositionZ());
float angle = bot->GetAngle(water.GetPositionX(), water.GetPositionY());
WorldPosition landSpot =
FindLandFromPosition(botAI, 0.0f, MAX_DISTANCE_TO_WATER, 1.0f, angle, water, fishingSearchWindow, false);
if (landSpot.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot", landSpot);
return true;
}
return false;
}
bool EquipFishingPoleAction::Execute(Event event)
{
if (!_pole)
return false;
WorldPacket eqPacket(CMSG_AUTOEQUIP_ITEM_SLOT, 2);
eqPacket << _pole->GetGUID() << uint8(EQUIPMENT_SLOT_MAINHAND);
WorldPackets::Item::AutoEquipItemSlot nicePacket(std::move(eqPacket));
nicePacket.Read();
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
return true;
}
bool EquipFishingPoleAction::isUseful()
{
Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
if (IsFishingPole(mainHand))
return false;
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
{
if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
{
if (IsFishingPole(item))
{
_pole = item;
return true;
}
}
}
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
if (Bag* pBag = bot->GetBagByPos(bag))
{
for (uint32 j = 0; j < pBag->GetBagSize(); ++j)
{
if (Item* item = pBag->GetItemByPos(j))
{
if (IsFishingPole(item))
{
_pole = item;
return true;
}
}
}
}
}
if (sRandomPlayerbotMgr->IsRandomBot(bot))
{
bot->StoreNewItemInBestSlots(FISHING_POLE, 1); // Try to get a fishing pole
return true;
}
Player* master = botAI->GetMaster();
if (!master)
return false;
std::string masterName = master->GetName();
std::string text = sPlayerbotTextMgr->GetBotTextOrDefault(
"no_fishing_pole_error", "I don't have a Fishing Pole",{});
botAI->Whisper(text, masterName);
return false;
}
bool FishingAction::Execute(Event event)
{
WorldPosition target = WorldPosition();
WorldPosition fishingHole = FindFishingHole(botAI);
if (fishingHole.IsValid())
{
Position pos = fishingHole;
float distance = bot->GetExactDist2d(&pos);
bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ());
if (distance < MAX_DISTANCE_TO_WATER &&
distance > MIN_DISTANCE_TO_WATER && hasLOS)
target = fishingHole;
}
if (!target.IsValid())
{
target = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), bot->GetMap(), bot->GetPhaseMask(),
MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, true, 32);
if (!target.IsValid())
return false;
}
Position pos = target;
if (!bot->HasInArc(1.0, &pos, 1.0))
{
float angle = bot->GetAngle(pos.GetPositionX(), pos.GetPositionY());
bot->SetOrientation(angle);
if (!bot->IsRooted())
bot->SendMovementFlagUpdate();
}
EquipFishingPoleAction equipAction(botAI);
if (equipAction.isUseful())
return equipAction.Execute(event);
botAI->CastSpell(FISHING_SPELL, bot);
botAI->ChangeStrategy("+use bobber", BOT_STATE_NON_COMBAT);
return true;
}
bool FishingAction::isUseful()
{
if (!AI_VALUE(bool, "can fish"))
return false;
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
if (!pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT))
return false;
return bot->GetExactDist(&pos) < 0.1f;
}
bool UseBobberAction::isUseful()
{
return AI_VALUE(bool, "can use fishing bobber");
}
bool UseBobberAction::Execute(Event event)
{
GuidVector gos = AI_VALUE(GuidVector, "nearest game objects no los");
for (auto const& guid : gos)
{
if (GameObject* go = botAI->GetGameObject(guid))
{
if (go->GetEntry() != FISHING_BOBBER)
continue;
if (go->GetOwnerGUID() != bot->GetGUID())
continue;
if (go->getLootState() == GO_READY)
{
go->Use(bot);
botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT);
return true;
}
}
}
return false;
}
bool EndMasterFishingAction::Execute(Event event)
{
botAI->ChangeStrategy("-master fishing", BOT_STATE_NON_COMBAT);
return true;
}
bool EndMasterFishingAction::isUseful()
{
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
if (pos.IsValid() && !fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) && pos == bot->GetPosition())
return false;
WorldPosition nearWater = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
bot->GetMap(), bot->GetPhaseMask(), MIN_DISTANCE_TO_WATER, sPlayerbotAIConfig->endFishingWithMaster, 10.0f);
return !nearWater.IsValid();
}
bool RemoveBobberStrategyAction::Execute(Event event)
{
botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT);
return true;
}

View File

@@ -0,0 +1,71 @@
/*
* 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_FISHINGACTION_H
#define _PLAYERBOT_FISHINGACTION_H
#include "Action.h"
#include "MovementActions.h"
#include "Event.h"
#include "Playerbots.h"
extern const uint32 FISHING_SPELL;
extern const uint32 FISHING_POLE;
extern const uint32 FISHING_BOBBER;
WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS=false, int numDirections = 16);
class PlayerbotAI;
class FishingAction : public Action
{
public:
FishingAction(PlayerbotAI* botAI) : Action(botAI, "go fishing"){}
bool Execute(Event event) override;
bool isUseful() override;
};
class EquipFishingPoleAction : public Action
{
public:
EquipFishingPoleAction(PlayerbotAI* botAI) : Action(botAI, "equip fishing pole") {}
bool Execute(Event event) override;
bool isUseful() override;
private:
Item* _pole = nullptr;
};
class MoveNearWaterAction : public MovementAction
{
public:
MoveNearWaterAction(PlayerbotAI* botAI): MovementAction(botAI, "move near water") {}
bool Execute(Event event) override;
bool isUseful() override;
bool isPossible() override;
};
class UseBobberAction : public Action
{
public:
UseBobberAction(PlayerbotAI* botAI) : Action(botAI, "use fishing bobber") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class EndMasterFishingAction : public Action
{
public:
EndMasterFishingAction(PlayerbotAI* botAI) : Action(botAI, "end master fishing") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class RemoveBobberStrategyAction : public Action
{
public:
RemoveBobberStrategyAction(PlayerbotAI* botAI) : Action(botAI, "remove bobber strategy") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -70,7 +70,7 @@ bool FollowAction::isUseful()
if (!target.empty()) if (!target.empty())
fTarget = AI_VALUE(Unit*, target); fTarget = AI_VALUE(Unit*, target);
else else
fTarget = AI_VALUE(Unit*, "master target"); fTarget = AI_VALUE(Unit*, "group leader");
if (fTarget) if (fTarget)
{ {
@@ -97,6 +97,8 @@ bool FollowAction::isUseful()
distance = bot->GetDistance(loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()); distance = bot->GetDistance(loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ());
} }
if (botAI->HasStrategy("master fishing", BOT_STATE_NON_COMBAT))
return sServerFacade->IsDistanceGreaterThan(distance, sPlayerbotAIConfig->fishingDistanceFromMaster);
return sServerFacade->IsDistanceGreaterThan(distance, formation->GetMaxDistance()); return sServerFacade->IsDistanceGreaterThan(distance, formation->GetMaxDistance());
} }
@@ -114,9 +116,9 @@ bool FollowAction::CanDeadFollow(Unit* target)
return true; return true;
} }
bool FleeToMasterAction::Execute(Event event) bool FleeToGroupLeaderAction::Execute(Event event)
{ {
Unit* fTarget = AI_VALUE(Unit*, "master target"); Unit* fTarget = AI_VALUE(Unit*, "group leader");
bool canFollow = Follow(fTarget); bool canFollow = Follow(fTarget);
if (!canFollow) if (!canFollow)
{ {
@@ -146,22 +148,22 @@ bool FleeToMasterAction::Execute(Event event)
return true; return true;
} }
bool FleeToMasterAction::isUseful() bool FleeToGroupLeaderAction::isUseful()
{ {
if (!botAI->GetGroupMaster()) if (!botAI->GetGroupLeader())
return false; return false;
if (botAI->GetGroupMaster() == bot) if (botAI->GetGroupLeader() == bot)
return false; return false;
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (target && botAI->GetGroupMaster()->GetTarget() == target->GetGUID()) if (target && botAI->GetGroupLeader()->GetTarget() == target->GetGUID())
return false; return false;
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
return false; return false;
Unit* fTarget = AI_VALUE(Unit*, "master target"); Unit* fTarget = AI_VALUE(Unit*, "group leader");
if (!CanDeadFollow(fTarget)) if (!CanDeadFollow(fTarget))
return false; return false;

View File

@@ -20,10 +20,10 @@ public:
bool CanDeadFollow(Unit* target); bool CanDeadFollow(Unit* target);
}; };
class FleeToMasterAction : public FollowAction class FleeToGroupLeaderAction : public FollowAction
{ {
public: public:
FleeToMasterAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to master") {} FleeToGroupLeaderAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to group leader") {}
bool Execute(Event event) override; bool Execute(Event event) override;
bool isUseful() override; bool isUseful() override;

View File

@@ -265,11 +265,6 @@ CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "s
} }
} }
NextAction** CastSpellAction::getPrerequisites()
{
return nullptr;
}
Value<Unit*>* CastDebuffSpellOnAttackerAction::GetTargetValue() Value<Unit*>* CastDebuffSpellOnAttackerAction::GetTargetValue()
{ {
return context->GetValue<Unit*>("attacker without aura", spell); return context->GetValue<Unit*>("attacker without aura", spell);

View File

@@ -27,7 +27,11 @@ public:
bool isUseful() override; bool isUseful() override;
ActionThreatType getThreatType() override { return ActionThreatType::Single; } ActionThreatType getThreatType() override { return ActionThreatType::Single; }
NextAction** getPrerequisites() override; std::vector<NextAction> getPrerequisites() override
{
return {};
}
std::string const getSpell() { return spell; } std::string const getSpell() { return spell; }
protected: protected:
@@ -193,10 +197,12 @@ public:
ResurrectPartyMemberAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} ResurrectPartyMemberAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
std::string const GetTargetName() override { return "party member to resurrect"; } std::string const GetTargetName() override { return "party member to resurrect"; }
NextAction** getPrerequisites() override std::vector<NextAction> getPrerequisites() override
{ {
return NextAction::merge(NextAction::array(0, new NextAction("reach party member to resurrect"), NULL), return NextAction::merge(
Action::getPrerequisites()); { NextAction("reach party member to resurrect") },
Action::getPrerequisites()
);
} }
}; };

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