mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
80 Commits
400c563e3d
...
0c9131692c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c9131692c | ||
|
|
1601d6a514 | ||
|
|
dd05767dcc | ||
|
|
7278a3bfcb | ||
|
|
ecbf3fdec2 | ||
|
|
01ea88624a | ||
|
|
02844dffd4 | ||
|
|
2597880d38 | ||
|
|
f4d308b684 | ||
|
|
a0e21d9f38 | ||
|
|
ed9e7227fb | ||
|
|
72d9ecabb9 | ||
|
|
d9a8ac3a2a | ||
|
|
8cb54416bf | ||
|
|
558e9ee1e1 | ||
|
|
563a415532 | ||
|
|
126294cc38 | ||
|
|
3b106260ac | ||
|
|
b3a8d9f4be | ||
|
|
3384fa4fcf | ||
|
|
0d3d38b007 | ||
|
|
4e8e3e2afe | ||
|
|
8c027e3a70 | ||
|
|
896ad3bf75 | ||
|
|
bdefd38830 | ||
|
|
5f61fe9ddf | ||
|
|
82ebaa9594 | ||
|
|
51cea4d76c | ||
|
|
aae47b06c7 | ||
|
|
d72d3ded6c | ||
|
|
974faf0cb0 | ||
|
|
e052ec3b17 | ||
|
|
4a991c194d | ||
|
|
ed31f8f8a7 | ||
|
|
479794b66b | ||
|
|
337fbca8c0 | ||
|
|
fe12f1a708 | ||
|
|
8916cf83c0 | ||
|
|
77caf85fd1 | ||
|
|
5e5d41f878 | ||
|
|
63c5d674d6 | ||
|
|
43ee732003 | ||
|
|
f42f37399f | ||
|
|
c7929482c4 | ||
|
|
e1489f213e | ||
|
|
007189fd5c | ||
|
|
eabefb1d33 | ||
|
|
2208c80caa | ||
|
|
a472fc2d68 | ||
|
|
bac63e2a8c | ||
|
|
690288b5cc | ||
|
|
57134918cb | ||
|
|
324b50f1be | ||
|
|
7d8d8c6b31 | ||
|
|
34b0432aaa | ||
|
|
edc999c8ac | ||
|
|
85e2a940a1 | ||
|
|
6754a95890 | ||
|
|
d0fac16c85 | ||
|
|
a64c721f35 | ||
|
|
27503a9c37 | ||
|
|
1a7e6db0c9 | ||
|
|
6ae973bb8e | ||
|
|
101da6ecd3 | ||
|
|
605e7586c5 | ||
|
|
129cb252cf | ||
|
|
088537277c | ||
|
|
6944da8d69 | ||
|
|
980c1b8cd8 | ||
|
|
ad14420400 | ||
|
|
1d0aeec7b9 | ||
|
|
7741626631 | ||
|
|
806013a4c9 | ||
|
|
3269d1a4b3 | ||
|
|
1ae72b0888 | ||
|
|
0a9bf70305 | ||
|
|
e18fdd02cd | ||
|
|
a23158ef52 | ||
|
|
4a63ee37e2 | ||
|
|
9bba4b78dd |
51
.claude/settings.json
Normal file
51
.claude/settings.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(awk '-F[\\(\\),]' ' *)",
|
||||||
|
"Bash(xargs ls -lah)",
|
||||||
|
"Bash(py --version)",
|
||||||
|
"Bash(py -3 --version)",
|
||||||
|
"Bash(py scripts/import_cmangos_travel_nodes.py)",
|
||||||
|
"Read(//home/dev/azerothcore_installer/_server/azerothcore/modules/mod-playerbots/data/sql/playerbots/updates/**)",
|
||||||
|
"Bash(py scripts/fixup_id_collision.py)",
|
||||||
|
"Bash(py scripts/insert_ignore.py)",
|
||||||
|
"Bash(grep -nA 6 \"passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Include/DetourNavMeshQuery.h)",
|
||||||
|
"Bash(grep -nA 5 \"dtQueryFilter::passFilter\" C:/Users/Admin/git/main/azerothcore-wotlk/deps/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp)",
|
||||||
|
"Bash(grep -nB1 -A2 \"modAlmostUnwalkableTriangles\" C:/Users/Admin/git/main/azerothcore-wotlk/src/tools/mmaps_generator/MapBuilder.cpp)",
|
||||||
|
"Bash(xargs grep -l \"MoveFarTo\\\\|ResolveMovePath\\\\|DispatchMovement\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.h)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push)",
|
||||||
|
"Bash(wait)",
|
||||||
|
"Bash(git -C \"c:/Users/Admin/git/main/azerothcore-wotlk/modules/mod-playerbots\" show 3710c35a)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" show 9cccc5d26)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Bias NAV_WATER 10x in default Player path filter\\\\\"\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"Revert \\\\\"feat\\(Core/Movement\\): Expose dtQueryFilter::setAreaCost via PathGenerator\\\\\"\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3..HEAD --stat)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" reset --hard cf9c6e9f3353d386f398cbe7a821abfd8fe9a4b3)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" push --force-with-lease)",
|
||||||
|
"Bash(git cherry-pick *)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" status -s)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" diff --stat)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" add src/server/game/Movement/MovementGenerators/PathGenerator.cpp src/server/game/Movement/MovementGenerators/PathGenerator.h)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"feat\\(Core/Movement\\): Cap Player path filter at 50° + water bias under MOD_PLAYERBOTS\")",
|
||||||
|
"Bash(grep -n \"CollectIncludeDirectories\\\\|sourcePath\\\\|GLOB.*\\\\\\\\.cpp\\\\\\\\|target_include\" C:/Users/Admin/git/main/azerothcore-wotlk/modules/CMakeLists.txt)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" commit -m \"fix\\(Core/Movement\\): Gate playerbot path filter on WorldSession::IsBot\\(\\)\")",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" fetch origin fix/mmaps-config-overrides-and-aliases)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline HEAD..origin/fix/mmaps-config-overrides-and-aliases)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" rebase origin/fix/mmaps-config-overrides-and-aliases)",
|
||||||
|
"Bash(git -C \"C:/Users/Admin/git/main/azerothcore-wotlk\" log --oneline -3)",
|
||||||
|
"Bash(grep -v \"//\")",
|
||||||
|
"Bash(grep -v \"^//\")"
|
||||||
|
],
|
||||||
|
"additionalDirectories": [
|
||||||
|
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\common\\Collision\\Maps",
|
||||||
|
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\tools\\mmaps_generator",
|
||||||
|
"C:\\Users\\Admin\\git\\main\\azerothcore-wotlk\\src\\server\\game\\Movement\\MovementGenerators"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
README.md
10
README.md
@ -48,6 +48,16 @@ Then build the server following the platform-specific instructions in our **[Ins
|
|||||||
|
|
||||||
> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues.
|
> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues.
|
||||||
|
|
||||||
|
### Required server configuration
|
||||||
|
|
||||||
|
In `worldserver.conf` (AzerothCore core config), set:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
PreloadAllNonInstancedMapGrids = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
This is required for `mod-playerbots`.
|
||||||
|
|
||||||
### Detailed Guides
|
### Detailed Guides
|
||||||
|
|
||||||
| Guide | Description |
|
| Guide | Description |
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
# THRESHOLDS
|
# THRESHOLDS
|
||||||
# QUESTS
|
# QUESTS
|
||||||
# COMBAT
|
# COMBAT
|
||||||
# GREATER BUFFS STRATEGIES
|
|
||||||
# CHEATS
|
# CHEATS
|
||||||
# SPELLS
|
# SPELLS
|
||||||
# FLIGHTPATH
|
# FLIGHTPATH
|
||||||
@ -343,10 +342,6 @@ AiPlayerbot.MaxWaitForMove = 5000
|
|||||||
# 2 - MoveSplinePath disabled everywhere
|
# 2 - MoveSplinePath disabled everywhere
|
||||||
AiPlayerbot.DisableMoveSplinePath = 0
|
AiPlayerbot.DisableMoveSplinePath = 0
|
||||||
|
|
||||||
# Max search time for movement (higher for better movement on slopes)
|
|
||||||
# Default: 3
|
|
||||||
AiPlayerbot.MaxMovementSearchTime = 3
|
|
||||||
|
|
||||||
# Action expiration time
|
# Action expiration time
|
||||||
AiPlayerbot.ExpireActionTime = 5000
|
AiPlayerbot.ExpireActionTime = 5000
|
||||||
|
|
||||||
@ -478,6 +473,23 @@ AiPlayerbot.AutoSaveMana = 1
|
|||||||
# Default: 60 (60%)
|
# Default: 60 (60%)
|
||||||
AiPlayerbot.SaveManaThreshold = 60
|
AiPlayerbot.SaveManaThreshold = 60
|
||||||
|
|
||||||
|
# Enable Paladin bots to use greater blessings, with the blessing used being based on the
|
||||||
|
# number of Paladins in the raid/group and the spec of the recipient. Priorities for each
|
||||||
|
# spec are hardcoded in GreaterBlessingActions.h.
|
||||||
|
# 0 = disabled
|
||||||
|
# 1 = enabled in raid groups only
|
||||||
|
# 2 = enabled in all groups
|
||||||
|
# Default: 1 (raid only)
|
||||||
|
AiPlayerbot.AutoGreaterBlessings = 1
|
||||||
|
|
||||||
|
# Enable bots to use group reagent buffs: Gift of the Wild, Arcane Brilliance,
|
||||||
|
# Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection.
|
||||||
|
# 0 = disabled
|
||||||
|
# 1 = enabled in raid groups only
|
||||||
|
# 2 = enabled in all groups
|
||||||
|
# Default: 2 (all groups)
|
||||||
|
AiPlayerbot.AutoPartyBuffs = 2
|
||||||
|
|
||||||
# Bots can flee from enemies
|
# Bots can flee from enemies
|
||||||
AiPlayerbot.FleeingEnabled = 1
|
AiPlayerbot.FleeingEnabled = 1
|
||||||
|
|
||||||
@ -486,24 +498,6 @@ AiPlayerbot.FleeingEnabled = 1
|
|||||||
#
|
#
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
|
||||||
####################################################################################################
|
|
||||||
# GREATER BUFFS STRATEGIES
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
# Min group size to use Greater buffs (Paladin, Mage, Druid)
|
|
||||||
# Default: 3
|
|
||||||
AiPlayerbot.MinBotsForGreaterBuff = 3
|
|
||||||
|
|
||||||
# Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff
|
|
||||||
# Default: 30
|
|
||||||
AiPlayerbot.RPWarningCooldown = 30
|
|
||||||
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
####################################################################################################
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
# CHEATS
|
# CHEATS
|
||||||
#
|
#
|
||||||
@ -1065,6 +1059,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2
|
|||||||
# Default: 1 (enabled)
|
# Default: 1 (enabled)
|
||||||
AiPlayerbot.EnableNewRpgStrategy = 1
|
AiPlayerbot.EnableNewRpgStrategy = 1
|
||||||
|
|
||||||
|
# Use pre-computed travel node paths for long-distance movement (>300 yards).
|
||||||
|
# When enabled, bots use the travel node graph (A*, flight paths, transports)
|
||||||
|
# instead of repeated mmap hops. Experimental.
|
||||||
|
# Default: 0 (disabled)
|
||||||
|
AiPlayerbot.EnableTravelNodes = 0
|
||||||
|
|
||||||
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
# Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.
|
||||||
# Sum of weights need not be 100. Set to 0 to disable the status.
|
# Sum of weights need not be 100. Set to 0 to disable the status.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -0,0 +1,632 @@
|
|||||||
|
-- Imported from cmangos-playerbots ai_playerbot_travelnode (wotlk)
|
||||||
|
-- 626 new nodes (cmangos has them, we didn't)
|
||||||
|
-- Matched on (name, mapId) + closest position; remaining are unique to cmangos.
|
||||||
|
|
||||||
|
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
|
||||||
|
(3781, ' portal', 0, 3476.3600, -4493.3600, 137.4900, 1),
|
||||||
|
(3782, ' portal', 530, 6107.2600, -6990.6400, 133.3170, 1),
|
||||||
|
(3783, ' spirithealer', 609, 1886.7800, -5784.5900, 102.8610, 1),
|
||||||
|
(3784, ' spirithealer', 609, 2116.1900, -5286.9400, 81.2151, 1),
|
||||||
|
(3785, ' spirithealer', 609, 2364.4200, -5771.3200, 151.3670, 1),
|
||||||
|
(3786, 'Absalan the Pious', 623, 36.0312, 40.4600, 25.0322, 1),
|
||||||
|
(3787, 'Acherus: The Ebon Hold flightMaster', 0, 2348.6300, -5669.2900, 382.3240, 1),
|
||||||
|
(3788, 'Acherus: The Ebon Hold spirithealer', 0, 2356.6500, -5663.0600, 382.2570, 1),
|
||||||
|
(3789, 'Alliance Log Ride 01 Begin', 571, 4274.5300, -3055.5500, 319.4630, 1),
|
||||||
|
(3790, 'Alliance PVP Barracks', 449, -9.1189, -4.2670, 5.5710, 1),
|
||||||
|
(3791, 'Alterac Mountains The Headland', 0, -80.9055, -331.6660, 136.4710, 1),
|
||||||
|
(3792, 'Alterac Mountains', 0, 498.3190, -1076.6400, 195.8960, 1),
|
||||||
|
(3793, 'Arathi Highlands The Sanctum', 0, -1532.4300, -1882.5600, 69.5533, 1),
|
||||||
|
(3794, 'Arathi Highlands The Tower of Arathor', 0, -1774.4800, -1518.3900, 75.2667, 1),
|
||||||
|
(3795, 'Archmage Pentarus', 603, -718.4560, -57.1132, 429.9240, 1),
|
||||||
|
(3796, 'Area 52 innkeeper', 530, 3062.1500, 3701.8200, 142.5620, 1),
|
||||||
|
(3797, 'Area52 Transporter', 530, 3092.5800, 3644.7200, 143.1360, 1),
|
||||||
|
(3798, 'Ashenvale Kargathia Keep', 1, 2437.0600, -3543.6300, 98.3115, 1),
|
||||||
|
(3799, 'Auberdine innkeeper', 1, 6406.5100, 515.3670, 8.7257, 1),
|
||||||
|
(3800, 'Auchindoun: Auchenai Crypts', 558, 63.4074, -175.2640, 15.4378, 1),
|
||||||
|
(3801, 'Auchindoun: Mana-Tombs', 557, -220.0700, -177.6620, -0.9810, 1),
|
||||||
|
(3802, 'Auchindoun: Sethekk Halls', 556, -102.4290, 177.5860, 0.0932, 1),
|
||||||
|
(3803, 'Auchindoun: Shadow Labyrinth', 555, -272.2950, -140.0660, 8.1563, 1),
|
||||||
|
(3804, 'Balzaphon', 329, 3733.2700, -3480.1100, 131.0400, 1),
|
||||||
|
(3805, 'Blackfathom Deeps', 48, -570.3640, 0.8977, -47.1378, 1),
|
||||||
|
(3806, 'Blackrock Depths', 230, 870.1800, -239.3270, -71.6776, 1),
|
||||||
|
(3807, 'Blackrock Spire', 229, -16.0931, -392.4040, 48.5157, 1),
|
||||||
|
(3808, 'Blades Edge Mountains Circle of Blood', 530, 2879.1300, 5979.4500, 6.2402, 1),
|
||||||
|
(3809, 'Blades Edge Mountains Vimgols Circle', 530, 3279.9000, 4640.3600, 216.5280, 1),
|
||||||
|
(3810, 'Blood elf start', 530, 10349.6000, -6357.2900, 33.4026, 1),
|
||||||
|
(3811, 'Bloodmyst Isle The Hidden Reef', 530, -1148.6600, -11127.4000, -76.0074, 1),
|
||||||
|
(3812, 'Borean Tundra Coldarra', 571, 3917.0600, 6817.9200, 150.5070, 1),
|
||||||
|
(3813, 'Borean Tundra Naxxanar', 571, 3750.9300, 3583.8600, 353.1330, 1),
|
||||||
|
(3814, 'Burning Steppes Blackrock Mountain', 0, -7996.6300, -1013.4100, 131.8670, 1),
|
||||||
|
(3815, 'Cannon Charging (Port)', 530, 1920.1300, 5581.9000, 269.7220, 1),
|
||||||
|
(3816, 'Cannon Prep', 0, -9569.6000, -13.7809, 63.9459, 1),
|
||||||
|
(3817, 'Cannon Prep', 1, -1327.6600, 85.9815, 130.2070, 1),
|
||||||
|
(3818, 'Cannon Prep', 530, -1742.2500, 5457.4000, -11.9282, 1),
|
||||||
|
(3819, 'Chief Thunder-Skins', 230, 847.8390, -181.1150, -49.6707, 1),
|
||||||
|
(3820, 'Coilfang: Serpentshrine Cavern', 548, 135.1620, -488.7410, 0.8400, 1),
|
||||||
|
(3821, 'Coilfang: The Slave Pens', 547, -71.6723, -322.2440, -1.5438, 1),
|
||||||
|
(3822, 'Coilfang: The Steamvault', 545, -91.9829, -255.4900, -12.5306, 1),
|
||||||
|
(3823, 'Coilfang: The Underbog', 546, 108.0610, -198.7420, 50.1184, 1),
|
||||||
|
(3824, 'Coilskar Witch', 585, 139.1010, -104.6440, -20.9245, 1),
|
||||||
|
(3825, 'Crystalsong Forest The Azure Front', 571, 5434.6500, 729.6780, 186.6960, 1),
|
||||||
|
(3826, 'Crystalsong Forest The Great Tree', 571, 5895.1000, 1022.6900, 185.5390, 1),
|
||||||
|
(3827, 'Crystalsong Forest The Twilight Rivulet', 571, 5508.9200, 432.9360, 161.7470, 1),
|
||||||
|
(3828, 'Dalaran Portal to Caverns of Time', 1, -8164.8000, -4768.5000, 34.3000, 1),
|
||||||
|
(3829, 'Dalaran Portal to Caverns of Time', 571, 5781.4800, 841.2580, 680.3790, 1),
|
||||||
|
(3830, 'Dalaran Portal to Darnassus', 1, 9656.5400, 2518.2600, 1331.6600, 1),
|
||||||
|
(3831, 'Dalaran Portal to Darnassus', 571, 5706.1600, 730.1020, 641.7450, 1),
|
||||||
|
(3832, 'Dalaran Portal to Exodar', 571, 5699.5800, 735.4690, 641.7690, 1),
|
||||||
|
(3833, 'Dalaran Portal to Ironforge', 0, -4613.7100, -915.2870, 501.0620, 1),
|
||||||
|
(3834, 'Dalaran Portal to Ironforge', 571, 5712.6800, 724.8450, 641.7360, 1),
|
||||||
|
(3835, 'Dalaran Portal to Orgrimmar', 1, 1470.0200, -4222.5900, 59.2213, 1),
|
||||||
|
(3836, 'Dalaran Portal to Orgrimmar', 571, 5925.8500, 593.2500, 640.5630, 1),
|
||||||
|
(3837, 'Dalaran Portal to Shattrath', 530, -1824.3200, 5417.2300, -12.4277, 1),
|
||||||
|
(3838, 'Dalaran Portal to Shattrath', 530, -1902.4900, 5442.8600, -12.4280, 1),
|
||||||
|
(3839, 'Dalaran Portal to Shattrath', 571, 5697.4900, 744.9120, 641.8190, 1),
|
||||||
|
(3840, 'Dalaran Portal to Shattrath', 571, 5941.6600, 584.8870, 640.5740, 1),
|
||||||
|
(3841, 'Dalaran Portal to Silvermoon', 530, 9998.4600, -7106.5500, 47.7054, 1),
|
||||||
|
(3842, 'Dalaran Portal to Silvermoon', 571, 5946.9800, 568.4790, 640.5730, 1),
|
||||||
|
(3843, 'Dalaran Portal to Stormwind', 571, 5719.1900, 719.6810, 641.7280, 1),
|
||||||
|
(3844, 'Dalaran Portal to Thunder Bluff', 1, -967.3750, 284.8200, 110.7730, 1),
|
||||||
|
(3845, 'Dalaran Portal to Thunder Bluff', 571, 5945.8100, 577.3570, 640.5740, 1),
|
||||||
|
(3846, 'Dalaran Portal to Undercity', 571, 5934.6600, 590.6880, 640.5750, 1),
|
||||||
|
(3847, 'Dalaran Violet Citadel Balcony', 571, 5865.0800, 840.2100, 846.3330, 1),
|
||||||
|
(3848, 'Darnassus The Temple Gardens', 1, 9814.7800, 2574.2100, 1314.4700, 1),
|
||||||
|
(3849, 'Darnassus Warriors Terrace', 1, 9976.2100, 2262.3700, 1334.5000, 1),
|
||||||
|
(3850, 'Darnassus innkeeper', 1, 9951.8800, 2282.8200, 1345.1600, 1),
|
||||||
|
(3851, 'Deadwind Pass Groshgok Compound', 0, -11121.6000, -2416.2600, 108.6530, 1),
|
||||||
|
(3852, 'Deadwind Pass Karazhan', 0, -11123.3000, -2006.7700, 47.2725, 1),
|
||||||
|
(3853, 'Deadwind Pass', 0, -10377.2000, -1785.4200, 94.3243, 1),
|
||||||
|
(3854, 'Deeprun Tram', 369, 13.6699, 80.6694, -4.2973, 1),
|
||||||
|
(3855, 'Desolace Bolgans Hole', 1, -2375.9500, 2469.7600, 74.4626, 1),
|
||||||
|
(3856, 'Desolace The Veiled Sea', 1, -1869.3900, 3404.3400, -50.7694, 1),
|
||||||
|
(3857, 'Dire Maul', 429, 135.5790, 192.0480, -3.3910, 1),
|
||||||
|
(3858, 'Doodad_CF_elevatorPlatform01entry', 548, 50.0000, -0.0071, -70.9017, 1),
|
||||||
|
(3859, 'Doodad_CF_elevatorPlatform_small01entry', 548, -59.1350, -98.7966, -51.7018, 1),
|
||||||
|
(3860, 'Doodad_FactoryElevator01entry', 554, 0.5438, -1.3935, -1.7012, 1),
|
||||||
|
(3861, 'Doodad_HF_Elevator_Gate01entry', 571, 254.1330, -5892.0000, 258.4950, 1),
|
||||||
|
(3862, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 167.2690, 1),
|
||||||
|
(3863, 'Doodad_HF_Elevator_Gate02', 571, 263.1630, -5919.2500, 173.3200, 1),
|
||||||
|
(3864, 'Doodad_HF_Elevator_Gate02entry', 571, 275.2000, -5918.1300, 166.4950, 1),
|
||||||
|
(3865, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 291.7530, 1),
|
||||||
|
(3866, 'Doodad_HF_Elevator_Gate03', 571, 135.1250, -5765.3500, 286.0490, 1),
|
||||||
|
(3867, 'Doodad_HF_Elevator_Gate03entry', 571, 133.3330, -5756.2700, 285.5940, 1),
|
||||||
|
(3868, 'Doodad_HF_Elevator_Lift01entry', 571, 259.9700, -5893.8300, 255.8280, 1),
|
||||||
|
(3869, 'Doodad_HF_Elevator_Lift01entry', 571, 261.1220, -5910.0100, 77.0138, 1),
|
||||||
|
(3870, 'Doodad_HF_Elevator_Lift01entry', 571, 261.9400, -5918.9400, 164.0950, 1),
|
||||||
|
(3871, 'Doodad_HF_Elevator_Lift_01entry', 571, 158.9330, -5760.0000, 38.3942, 1),
|
||||||
|
(3872, 'Doodad_ID_elevator01entry', 571, 3324.2300, -5134.7400, 300.5890, 1),
|
||||||
|
(3873, 'Doodad_ID_elevator02entry', 571, 3302.5400, -5103.7300, 300.5890, 1),
|
||||||
|
(3874, 'Doodad_ID_elevator03entry', 571, 3286.7600, -5135.8800, 300.5890, 1),
|
||||||
|
(3875, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 336.1010, 1),
|
||||||
|
(3876, 'Doodad_LogRun_PumpElevator05', 571, 4264.2300, -3276.9500, 330.6150, 1),
|
||||||
|
(3877, 'Doodad_LogRun_PumpElevator05entry', 571, 4264.2300, -3276.9500, 329.3720, 1),
|
||||||
|
(3878, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3556.2500, 6928.7200, 251.3120, 1),
|
||||||
|
(3879, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 3979.2300, 7272.5600, 256.0160, 1),
|
||||||
|
(3880, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 571, 4110.2800, 6755.6900, 249.1790, 1),
|
||||||
|
(3881, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1213.8600, 1339.0600, 248.2530, 1),
|
||||||
|
(3882, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 1343.4300, 825.8630, 242.8180, 1),
|
||||||
|
(3883, 'Doodad_Nexus_Elevator_BaseStructure_01entry', 578, 789.8890, 992.0980, 251.2600, 1),
|
||||||
|
(3884, 'Doodad_Vrykul_Gondola01entry', 571, 698.9930, -3824.2500, 269.5360, 1),
|
||||||
|
(3885, 'Doodad_Vrykul_Gondola01entryentry', 571, 700.2670, -3823.5000, 269.3790, 1),
|
||||||
|
(3886, 'Doodad_Vrykul_Gondola_01entry', 575, -557.0330, 1543.4600, -288.9820, 1),
|
||||||
|
(3887, 'Doodad_icecrown_elevator02entry', 631, 4366.6900, 2365.6200, 358.4790, 1),
|
||||||
|
(3888, 'Doodad_mushroombase_elevator01entry', 530, 285.6000, 5927.2000, 26.6102, 1),
|
||||||
|
(3889, 'Doodad_org_arena_axe_pillar01entry', 618, 768.0000, -298.6670, 28.4867, 1),
|
||||||
|
(3890, 'Doodad_org_arena_lightning_pillar01entry', 618, 768.0000, -277.3330, 28.4867, 1),
|
||||||
|
(3891, 'Draenei start', 530, -3961.6400, -13931.2000, 100.6150, 1),
|
||||||
|
(3892, 'Dragonblight Frostmourne Cavern', 571, 4736.3900, -558.6370, 166.0830, 1),
|
||||||
|
(3893, 'Dragonblight Scarlet Tower', 571, 4692.0500, -356.1290, 178.7370, 1),
|
||||||
|
(3894, 'Dragonblight The Pit of Narjun', 571, 3740.0300, 2125.2500, 43.0709, 1),
|
||||||
|
(3895, 'Dragonblight Wintergarde Crypt', 571, 3584.5200, -781.0240, 155.6860, 1),
|
||||||
|
(3896, 'Dragonblight Wintergarde Mausoleum', 571, 3650.8400, -1123.3200, 89.2307, 1),
|
||||||
|
(3897, 'Drakuramas Teleport 02', 571, 6175.5900, -2000.6700, 241.7690, 1),
|
||||||
|
(3898, 'Dun Morogh Chill Breeze Valley', 0, -5550.9700, -76.1191, 426.9310, 1),
|
||||||
|
(3899, 'Dun Morogh Thunderbrew Distillery', 0, -5595.6700, -513.5950, 409.4150, 1),
|
||||||
|
(3900, 'Durotar Burning Blade Coven', 1, -88.3030, -4285.0100, 62.0652, 1),
|
||||||
|
(3901, 'Durotar Dustwind Cave', 1, 877.9880, -4745.6200, 30.4963, 1),
|
||||||
|
(3902, 'Durotar Razor Hill Barracks', 1, 319.0970, -4812.8800, 10.6054, 1),
|
||||||
|
(3903, 'Durotar The Den', 1, -588.7030, -4144.9400, 41.1033, 1),
|
||||||
|
(3904, 'Duskwood Forlorn Rowe', 0, -10320.8000, 368.2760, 60.5037, 1),
|
||||||
|
(3905, 'Duskwood Rolands Doom', 0, -11106.5000, -1160.6700, 42.2494, 1),
|
||||||
|
(3906, 'Dustwallow Marsh Emberstrifes Den', 1, -5103.3600, -3949.5300, 41.4934, 1),
|
||||||
|
(3907, 'Dustwallow Marsh Foothold Citadel', 1, -3721.7400, -4538.6000, 25.9170, 1),
|
||||||
|
(3908, 'Dustwallow Marsh North Point Tower', 1, -2885.3400, -3419.3200, 47.2420, 1),
|
||||||
|
(3909, 'Dustwallow Marsh The Great Sea', 1, -4026.5700, -4975.8200, 8.2153, 1),
|
||||||
|
(3910, 'Dustwallow to Stormwind Teleport', 0, -9008.7900, 851.3200, 105.8900, 1),
|
||||||
|
(3911, 'Eastern Plaguelands MazraAlor', 0, 3439.2500, -4980.9600, 195.8110, 1),
|
||||||
|
(3912, 'Eastern Plaguelands Terrorweb Tunnel', 0, 2903.1100, -2614.6700, 89.8071, 1),
|
||||||
|
(3913, 'Eastern Plaguelands The Noxious Glade', 0, 2714.6600, -5421.4700, 161.4070, 1),
|
||||||
|
(3914, 'Elevatorentry', 0, -5065.6200, 437.5100, 424.1080, 1),
|
||||||
|
(3915, 'Elevatorentry', 1, 2261.3300, -5565.5600, 34.2689, 1),
|
||||||
|
(3916, 'Elevatorentry', 530, 1914.5100, 5514.0200, 280.6880, 1),
|
||||||
|
(3917, 'Elevatorentry', 571, 2872.9500, 6234.9700, 104.9120, 1),
|
||||||
|
(3918, 'Elevatorentry', 571, 2894.6900, 6244.8900, 209.1970, 1),
|
||||||
|
(3919, 'Elevatorentry', 571, 4185.2900, 5283.6200, 39.6833, 1),
|
||||||
|
(3920, 'Elwynn Forest Thunder Falls', 0, -9291.1000, 677.6830, 131.7780, 1),
|
||||||
|
(3921, 'Entangle', 531, -8003.0000, 1222.9000, -82.1000, 1),
|
||||||
|
(3922, 'Entangle', 531, -8022.3000, 1149.0000, -89.1000, 1),
|
||||||
|
(3923, 'Entangle', 531, -8043.6000, 1254.1000, -84.3000, 1),
|
||||||
|
(3924, 'Eredar Soul-Eater', 552, 285.5190, 146.1550, 22.3118, 1),
|
||||||
|
(3925, 'Escape Voltarus', 571, 5875.4300, -1981.3700, 234.6710, 1),
|
||||||
|
(3926, 'Escape to the Isle of QuelDanas', 530, 12887.6000, -6869.2100, 10.1141, 1),
|
||||||
|
(3927, 'Escape to the Isle of QuelDanas', 585, 148.4010, 203.4430, -11.9579, 1),
|
||||||
|
(3928, 'Evergrove innkeeper', 530, 3022.9000, 5435.5900, 146.7010, 1),
|
||||||
|
(3929, 'Everlook Transporter', 1, 6755.2200, -4658.0400, 724.7950, 1),
|
||||||
|
(3930, 'Everlook innkeeper', 1, 6695.1500, -4673.0400, 721.6500, 1),
|
||||||
|
(3931, 'Eversong Woods Commons Hall', 530, 9822.0000, -6694.3700, 2.5945, 1),
|
||||||
|
(3932, 'Eversong Woods Falthrien Academy', 530, 10158.0000, -6026.5000, 63.7448, 1),
|
||||||
|
(3933, 'Eversong Woods Huntress of the Sun', 530, 9699.4300, -6701.3600, -0.2076, 1),
|
||||||
|
(3934, 'Exit Portal', 571, 3877.1400, 6980.8300, 152.0400, 1),
|
||||||
|
(3935, 'Feathermoon Stronghold innkeeper', 1, -4381.5900, 3289.4500, 13.6266, 1),
|
||||||
|
(3936, 'Felwood Bloodvenom River', 1, 5288.7400, -544.2570, 328.6870, 1),
|
||||||
|
(3937, 'Felwood Irontree Cavern', 1, 6353.3200, -1696.9200, 440.0420, 1),
|
||||||
|
(3938, 'Felwood Shrine of the Deceiver', 1, 4779.2600, -572.0400, 275.8440, 1),
|
||||||
|
(3939, 'Gadgetzan Transporter', 1, -7109.1200, -3825.2100, 10.1529, 1),
|
||||||
|
(3940, 'Gadgetzan innkeeper', 1, -7158.9600, -3841.6100, 8.8481, 1),
|
||||||
|
(3941, 'Gallows End Tavern innkeeper', 0, 2269.5100, 244.9440, 34.3402, 1),
|
||||||
|
(3942, 'Gates of AhnQiraj', 1, -8233.1200, 1535.7500, -0.3744, 1),
|
||||||
|
(3943, 'Ghostlands Deatholme', 530, 6465.7700, -6433.6600, 50.4155, 1),
|
||||||
|
(3944, 'Ghostlands Thalassian Pass', 530, 6554.0200, -6811.2000, 110.6000, 1),
|
||||||
|
(3945, 'Ghostly Baker', 532, -11057.7000, -1919.8300, 77.3515, 1),
|
||||||
|
(3946, 'Gnomeregan', 90, -617.4620, 370.6960, -247.1880, 1),
|
||||||
|
(3947, 'Gravity Lapse - Center Teleport', 585, 148.5000, 181.0000, -16.7000, 1),
|
||||||
|
(3948, 'Greater Shadowbat', 532, -10935.0000, -1999.5000, 49.4748, 1),
|
||||||
|
(3949, 'Grizzly Hills - Quest - Arugal Teleport Back', 571, 3841.4000, -3426.6500, 293.1040, 1),
|
||||||
|
(3950, 'Grizzly Hills Boulder Hills', 571, 5082.6400, -4724.1900, 287.5000, 1),
|
||||||
|
(3951, 'Grizzly Hills Duskhowl Den', 571, 3992.0100, -4523.4300, 195.6070, 1),
|
||||||
|
(3952, 'Grizzly Hills Ursocs Den', 571, 4692.7100, -3863.7500, 327.3770, 1),
|
||||||
|
(3953, 'Grizzly Hills Voldrune Dwelling', 571, 3005.7300, -2610.3100, 98.5530, 1),
|
||||||
|
(3954, 'Gruuls Lair', 565, 107.8710, 282.5120, 1.9718, 1),
|
||||||
|
(3955, 'Gunship Portal', 628, 747.0000, -1075.0000, 135.0000, 1),
|
||||||
|
(3956, 'Gunship Portal', 641, 16.4763, 0.0184, 20.4162, 1),
|
||||||
|
(3957, 'Gunship Portal', 642, 12.3199, 0.0963, 34.6508, 1),
|
||||||
|
(3958, 'Heart of the Deconstructor', 603, 886.2750, -12.0545, 409.6020, 1),
|
||||||
|
(3959, 'Hellfire Citadel: The Blood Furnace', 542, 331.0690, 28.0790, 9.7047, 1),
|
||||||
|
(3960, 'Hellfire Citadel: The Shattered Halls', 540, 203.6690, 157.2020, -42.3511, 1),
|
||||||
|
(3961, 'Hellfire Peninsula The Path of Anguish', 530, -525.2730, 2066.0700, 94.5205, 1),
|
||||||
|
(3962, 'Hex Lord Malacrass', 568, 117.3630, 923.5690, 33.9726, 1),
|
||||||
|
(3963, 'Hillsbrad Foothills Durnholde Keep', 0, -489.8100, -1480.9400, 88.1965, 1),
|
||||||
|
(3964, 'HiveZara Swarmer Teleport', 509, -9757.8700, 1416.7100, 76.7664, 1),
|
||||||
|
(3965, 'HiveZara Swarmer Teleport', 509, -9778.9100, 1419.9800, 61.0743, 1),
|
||||||
|
(3966, 'HiveZara Swarmer Teleport', 509, -9805.9500, 1422.8500, 77.5852, 1),
|
||||||
|
(3967, 'HiveZara Swarmer Teleport', 509, -9827.5800, 1506.2800, 82.3052, 1),
|
||||||
|
(3968, 'HiveZara Swarmer Teleport', 509, -9829.4200, 1456.3700, 90.7015, 1),
|
||||||
|
(3969, 'Horde Log Ride 01 Begin', 571, 4313.3700, -2958.1700, 318.4630, 1),
|
||||||
|
(3970, 'Howling Fjord New Agamand Inn', 571, 456.4610, -4519.2400, 244.9780, 1),
|
||||||
|
(3971, 'Howling Fjord Utgarde Keep', 571, 855.8150, -4831.8500, -115.7170, 1),
|
||||||
|
(3972, 'Howling Fjord Westguard Keep', 571, 1385.1700, -3244.6400, 162.8370, 1),
|
||||||
|
(3973, 'Howling Fjord Wildervar Mine', 571, 2604.4700, -5019.1400, 293.8260, 1),
|
||||||
|
(3974, 'Icecrown Jotunheim', 571, 7220.9200, 4113.1400, 630.8240, 1),
|
||||||
|
(3975, 'Icecrown Kulgalar Keep', 571, 6831.2300, 3817.0800, 621.1530, 1),
|
||||||
|
(3976, 'Icecrown Nazanak: The Forgotten Depths', 571, 5853.5100, 1807.1700, -344.9310, 1),
|
||||||
|
(3977, 'Icecrown The Fleshwerks', 571, 6610.5800, 3359.6700, 660.4740, 1),
|
||||||
|
(3978, 'Indisposed II', 571, 3454.1100, -2802.3700, 202.1400, 1),
|
||||||
|
(3979, 'Ironforge The Forlorn Cavern', 0, -4636.2200, -1110.0000, 501.4110, 1),
|
||||||
|
(3980, 'Ironforge flightMaster', 0, -4821.1300, -1152.4000, 502.2950, 1),
|
||||||
|
(3981, 'Ironforge innkeeper', 0, -4840.6700, -857.0940, 501.9970, 1),
|
||||||
|
(3982, 'Ironforge innkeeper', 0, -4908.1700, -970.9860, 505.2260, 1),
|
||||||
|
(3983, 'Ironforge', 0, -4832.2700, -1069.6400, 502.2680, 1),
|
||||||
|
(3984, 'Kalecgos', 585, 197.8630, -272.7440, -8.6516, 1),
|
||||||
|
(3985, 'Lady Faltheress', 129, 2583.1800, 695.8610, 56.8033, 1),
|
||||||
|
(3986, 'Lady Liadrin', 580, 1720.0200, 643.2330, 28.1335, 1),
|
||||||
|
(3987, 'Loch Modan Stoutlager Inn', 0, -5391.7300, -2974.8800, 325.9470, 1),
|
||||||
|
(3988, 'Lord Blackwood', 289, 200.2010, 150.8390, 109.8790, 1),
|
||||||
|
(3989, 'MaiKyl', 230, 842.7150, -181.5710, -49.6700, 1),
|
||||||
|
(3990, 'Majordomo Teleport', 409, 848.9330, -812.8750, -229.6010, 1),
|
||||||
|
(3991, 'Maraudon Portal Effect', 349, 386.2700, 33.4144, -130.9340, 1),
|
||||||
|
(3992, 'Maraudon', 349, 614.7090, -206.5360, -64.2963, 1),
|
||||||
|
(3993, 'Mesa Elevator', 209, 2600.8100, 1228.7200, -40.2788, 1),
|
||||||
|
(3994, 'Mesa Elevator', 209, 2600.8100, 1228.7200, 89.0211, 1),
|
||||||
|
(3995, 'Mesa Elevator', 209, 2617.5100, 1243.9200, -40.5284, 1),
|
||||||
|
(3996, 'Mesa Elevator', 209, 2617.5100, 1243.9200, 89.0253, 1),
|
||||||
|
(3997, 'Mesa Elevatorentry', 1, -1030.1400, -32.0337, 68.2329, 1),
|
||||||
|
(3998, 'Mesa Elevatorentry', 1, -1032.6600, -26.3241, 141.1470, 1),
|
||||||
|
(3999, 'Mesa Elevatorentry', 1, -1035.1900, -45.0725, 68.2586, 1),
|
||||||
|
(4000, 'Mesa Elevatorentry', 1, -1042.0500, -47.7792, 141.1470, 1),
|
||||||
|
(4001, 'Mesa Elevatorentry', 1, -1284.7700, 185.1110, 130.3230, 1),
|
||||||
|
(4002, 'Mesa Elevatorentry', 1, -1290.7100, 188.5430, 67.4105, 1),
|
||||||
|
(4003, 'Mesa Elevatorentry', 1, -1304.5600, 186.1990, 67.5106, 1),
|
||||||
|
(4004, 'Mesa Elevatorentry', 1, -1308.1800, 180.4040, 130.2800, 1),
|
||||||
|
(4005, 'Mesa Elevatorentry', 1, -4660.6000, -1828.2700, 85.6823, 1),
|
||||||
|
(4006, 'Mesa Elevatorentry', 1, -4666.2300, -1851.2600, 85.6813, 1),
|
||||||
|
(4007, 'Mesa Elevatorentry', 1, -4666.4800, -1832.0900, -45.3171, 1),
|
||||||
|
(4008, 'Mesa Elevatorentry', 1, -4670.1200, -1845.7300, -45.1181, 1),
|
||||||
|
(4009, 'Mesa Elevatorentry', 1, -5385.2000, -2485.3300, 89.4297, 1),
|
||||||
|
(4010, 'Mesa Elevatorentry', 1, -5385.2700, -2492.0100, -41.7147, 1),
|
||||||
|
(4011, 'Mesa Elevatorentry', 1, -5395.6800, -2501.7500, -41.5869, 1),
|
||||||
|
(4012, 'Mesa Elevatorentry', 1, -5402.5700, -2501.2400, 89.4297, 1),
|
||||||
|
(4013, 'Mesa Elevatorentry', 209, 2597.4300, 1232.1000, 89.4297, 1),
|
||||||
|
(4014, 'Mesa Elevatorentry', 209, 2604.3200, 1231.5800, -41.5870, 1),
|
||||||
|
(4015, 'Mesa Elevatorentry', 209, 2614.7300, 1241.3200, -41.7145, 1),
|
||||||
|
(4016, 'Mesa Elevatorentry', 209, 2614.8000, 1248.0000, 89.4297, 1),
|
||||||
|
(4017, 'Mesa Elevatorentry', 47, 1729.8800, 1354.2700, -45.1181, 1),
|
||||||
|
(4018, 'Mesa Elevatorentry', 47, 1733.5200, 1367.9100, -45.3171, 1),
|
||||||
|
(4019, 'Mesa Elevatorentry', 47, 1733.7700, 1348.7400, 85.6813, 1),
|
||||||
|
(4020, 'Mesa Elevatorentry', 47, 1739.4000, 1371.7300, 85.6823, 1),
|
||||||
|
(4021, 'Mole Machine Port to Grim Guzzler', 230, 901.0680, -143.9390, -49.7550, 1),
|
||||||
|
(4022, 'Molten Core', 409, 889.0990, -822.9660, -227.2440, 1),
|
||||||
|
(4023, 'Move Bind Sight', 230, 824.8090, -176.1660, -49.7551, 1),
|
||||||
|
(4024, 'Mudsprocket innkeeper', 1, -4629.4300, -3176.1400, 41.2339, 1),
|
||||||
|
(4025, 'Murta Grimgut', 209, 1891.0700, 1294.7800, 48.2347, 1),
|
||||||
|
(4026, 'Nagrand Abandoned Armory', 530, -2052.0900, 7432.7600, -24.9648, 1),
|
||||||
|
(4027, 'Naxxanar portal', 571, 3692.4300, 3577.9400, 473.3200, 1),
|
||||||
|
(4028, 'Naxxanar portal', 571, 3734.1400, 3571.1200, 341.6620, 1),
|
||||||
|
(4029, 'Naxxanar portal', 571, 3738.6400, 3567.6700, 294.5220, 1),
|
||||||
|
(4030, 'Naxxanar portal', 571, 3739.5000, 3567.0000, 337.5640, 1),
|
||||||
|
(4031, 'Naxxanar portal', 571, 3739.6600, 3567.2900, 286.7850, 1),
|
||||||
|
(4032, 'Naxxanar portal', 571, 3787.5700, 3558.1100, 469.3220, 1),
|
||||||
|
(4033, 'Naxxanar portal', 571, 3801.5000, 3586.0500, 49.5700, 1),
|
||||||
|
(4034, 'Naxxramas Teleport - Sapphiron Exit', 533, 3005.7300, -3414.7600, 297.0260, 1),
|
||||||
|
(4035, 'Netherstorm Manaforge Ara', 530, 3965.9500, 3911.9700, 178.4460, 1),
|
||||||
|
(4036, 'Netherstorm The Violet Tower', 530, 2243.2600, 2243.1400, 101.5590, 1),
|
||||||
|
(4037, 'Nexus Portal', 578, 1045.5700, 1104.2400, 361.0700, 1),
|
||||||
|
(4038, 'Nexus Portal', 578, 982.1730, 1055.4500, 359.9670, 1),
|
||||||
|
(4039, 'Nijels Point innkeeper', 1, 255.6150, 1253.7600, 192.2240, 1),
|
||||||
|
(4040, 'No Mans Land -> Out of Hyjal', 1, 5010.1700, -4554.9400, 852.1460, 1),
|
||||||
|
(4041, 'Onyxias Lair', 249, -90.7180, -106.2340, -38.1972, 1),
|
||||||
|
(4042, 'Orb of the Nexus', 578, 1048.2700, 991.3100, 361.0700, 1),
|
||||||
|
(4043, 'Orgrims Hammer', 622, 23.4324, 0.0201, 33.5795, 1),
|
||||||
|
(4044, 'Orgrims Hammerdock', 668, 5223.4700, 1537.8700, 645.6480, 1),
|
||||||
|
(4045, 'Orgrimmar flightMaster', 1, 1676.2500, -4313.4500, 61.9445, 1),
|
||||||
|
(4046, 'Out of Body Experience', 0, -465.6990, 1495.9800, 17.3595, 1),
|
||||||
|
(4047, 'Port to Mazthoril', 1, 5904.2000, -4045.9000, 596.4300, 1),
|
||||||
|
(4048, 'Portal Effect: Acherus', 609, 2400.0300, -5635.0000, 377.0170, 1),
|
||||||
|
(4049, 'Portal of Immolthar', 429, -37.5643, 813.4330, -7.4382, 1),
|
||||||
|
(4050, 'Portal to Blasted Lands', 0, -11708.4000, -3168.0000, -5.0700, 1),
|
||||||
|
(4051, 'Portal to Blasted Lands', 0, -4606.4400, -928.9970, 501.0700, 1),
|
||||||
|
(4052, 'Portal to Blasted Lands', 0, -9007.5800, 871.8700, 129.6920, 1),
|
||||||
|
(4053, 'Portal to Blasted Lands', 0, 1768.7700, 55.3698, -46.3194, 1),
|
||||||
|
(4054, 'Portal to Blasted Lands', 1, -944.0640, 274.8590, 111.7100, 1),
|
||||||
|
(4055, 'Portal to Blasted Lands', 1, 1472.0900, -4214.6300, 59.2208, 1),
|
||||||
|
(4056, 'Portal to Blasted Lands', 1, 9661.8300, 2509.6000, 1331.6300, 1),
|
||||||
|
(4057, 'Portal to Blasted Lands', 530, -4039.0700, -11555.1000, -138.3370, 1),
|
||||||
|
(4058, 'Portal to Blasted Lands', 530, 9984.0300, -7108.4300, 47.7049, 1),
|
||||||
|
(4059, 'Portal to Dalaran', 571, 5807.7500, 588.3470, 661.5050, 1),
|
||||||
|
(4060, 'Portal to Dalaran', 631, -72.7031, 2282.4500, 32.8673, 1),
|
||||||
|
(4061, 'Portal to Dalaran', 712, 4.3953, 13.6833, 20.8039, 1),
|
||||||
|
(4062, 'Portal to Dalaran', 713, 22.1770, 22.9527, 35.6576, 1),
|
||||||
|
(4063, 'Portal to Orgrimmar', 1, 1321.8100, -4383.1900, 26.2300, 1),
|
||||||
|
(4064, 'Portal to Orgrimmar', 1, 1921.3700, -4149.2400, 40.4075, 1),
|
||||||
|
(4065, 'Portal to Stormwind', 0, -8446.8300, 339.9310, 121.3290, 1),
|
||||||
|
(4066, 'Portal to Stormwind', 0, -9143.5800, 375.1030, 90.6907, 1),
|
||||||
|
(4067, 'Portal to The Purple Parlor', 571, 5822.3500, 837.0760, 680.6570, 1),
|
||||||
|
(4068, 'Portal to The Purple Parlor', 571, 5848.4800, 853.7060, 843.1820, 1),
|
||||||
|
(4069, 'Portal to The Violet Citadel', 571, 5413.0100, 2868.1800, 418.6750, 1),
|
||||||
|
(4070, 'Portal to The Violet Citadel', 571, 5819.2600, 829.7740, 680.2200, 1),
|
||||||
|
(4071, 'Portal to Undercity', 0, 1774.8000, 761.1270, 55.0477, 1),
|
||||||
|
(4072, 'Portal to Undercity', 0, 1962.6900, 235.9200, 39.7700, 1),
|
||||||
|
(4073, 'Portal: Moonglade Effect', 1, 7828.8400, -2245.6500, 463.7070, 1),
|
||||||
|
(4074, 'Portal: Return from Moonglade Effect', 571, 6215.5400, -8.4001, 410.1650, 1),
|
||||||
|
(4075, 'Portal: Valley of Echoes', 571, 6390.5300, 237.0120, 395.8130, 1),
|
||||||
|
(4076, 'Quest - Port Skettis Prisoner - Location 01', 530, -4106.6400, 3029.7600, 344.8770, 1),
|
||||||
|
(4077, 'Quest - Port Skettis Prisoner - Location 02', 530, -3720.3500, 3789.9100, 302.8880, 1),
|
||||||
|
(4078, 'Quest - Port Skettis Prisoner - Location 03', 530, -3669.5700, 3386.7400, 312.9550, 1),
|
||||||
|
(4079, 'Quest - Teleport: Caverns of Time', 1, -8380.5000, -4262.5600, -207.5340, 1),
|
||||||
|
(4080, 'Quetzlun', 571, 5716.2600, -4369.3400, 385.8850, 1),
|
||||||
|
(4081, 'Ragefire Chasm', 389, -223.4540, 87.2837, -24.9351, 1),
|
||||||
|
(4082, 'Raven', 209, 1886.6400, 1299.1100, 48.3146, 1),
|
||||||
|
(4083, 'Recall', 30, -1347.0000, -292.0000, 91.0000, 1),
|
||||||
|
(4084, 'Recall', 30, 648.0000, -34.0000, 47.0000, 1),
|
||||||
|
(4085, 'Redridge Mountains Renders Rock', 0, -8737.4300, -2213.5000, 149.9600, 1),
|
||||||
|
(4086, 'Redridge Mountains Stonewatch', 0, -9392.0000, -3026.6300, 136.7700, 1),
|
||||||
|
(4087, 'Return to Temper', 530, -3286.6300, -12908.6000, 11.7562, 1),
|
||||||
|
(4088, 'Revanchion', 429, -106.3030, 551.2760, -4.3970, 1),
|
||||||
|
(4089, 'Ritual Preparation', 575, 296.6890, -346.5040, 90.5482, 1),
|
||||||
|
(4090, 'Ritual of the Sword', 575, 296.6890, -346.5040, 108.5480, 1),
|
||||||
|
(4091, 'Rizzles Escape', 1, 3697.2000, -3967.1300, 28.3115, 1),
|
||||||
|
(4092, 'Sacrifice', 429, -25.9536, -448.2270, -36.0912, 1),
|
||||||
|
(4093, 'Sacrifice', 532, -11234.2000, -1698.4600, 179.2400, 1),
|
||||||
|
(4094, 'Sacrifice', 553, 4.0000, 608.0000, -13.8165, 1),
|
||||||
|
(4095, 'Samuro', 230, 847.6740, -175.8690, -49.6706, 1),
|
||||||
|
(4096, 'Scaffold Carsentry', 0, -6889.3100, -1211.7600, 240.4440, 1),
|
||||||
|
(4097, 'Scaffold Carsentry', 0, -6895.5000, -1338.1500, 240.2840, 1),
|
||||||
|
(4098, 'Scaffold Carsentry', 0, -6899.7000, -1207.6000, 240.4100, 1),
|
||||||
|
(4099, 'Scaffold Carsentry', 0, -6903.5800, -1201.6700, 193.8930, 1),
|
||||||
|
(4100, 'Scaffold Carsentry', 0, -6905.7300, -1211.8300, 214.0430, 1),
|
||||||
|
(4101, 'Scaffold Carsentry', 469, -6889.3100, -1211.7600, 240.4440, 1),
|
||||||
|
(4102, 'Scaffold Carsentry', 469, -6895.5000, -1338.1500, 240.2840, 1),
|
||||||
|
(4103, 'Scaffold Carsentry', 469, -6899.7000, -1207.6000, 240.4100, 1),
|
||||||
|
(4104, 'Scaffold Carsentry', 469, -6903.5800, -1201.6700, 193.8930, 1),
|
||||||
|
(4105, 'Scaffold Carsentry', 469, -6905.7300, -1211.8300, 214.0430, 1),
|
||||||
|
(4106, 'Scaffold Carsentry', 530, -3292.8900, 1901.2800, 142.1590, 1),
|
||||||
|
(4107, 'Scaffold Carsentry', 530, -3297.0700, 1898.6700, 101.3760, 1),
|
||||||
|
(4108, 'Scaffold Carsentry', 530, -3300.1400, 1902.2300, 168.4120, 1),
|
||||||
|
(4109, 'Scaffold Carsentry', 530, -3301.8300, 1895.4800, 122.1590, 1),
|
||||||
|
(4110, 'Scaffold Carsentry', 530, -3306.6700, 1902.6700, 168.5590, 1),
|
||||||
|
(4111, 'Scaffold Carsentry', 571, 8115.1400, -245.3940, 900.1890, 1),
|
||||||
|
(4112, 'Scaffold Carsentry', 571, 8118.4000, -380.0000, 1027.9200, 1),
|
||||||
|
(4113, 'Scaffold Carsentry', 571, 8118.7700, -386.8160, 981.7890, 1),
|
||||||
|
(4114, 'Scaffold Carsentry', 571, 8119.4000, -256.0000, 926.3570, 1),
|
||||||
|
(4115, 'Scaffold Carsentry', 571, 8119.6800, -379.0410, 1028.0500, 1),
|
||||||
|
(4116, 'Scaffold Carsentry', 571, 8122.6000, -247.5780, 926.3930, 1),
|
||||||
|
(4117, 'Scaffold Carsentry', 571, 8123.9300, -240.0710, 880.1890, 1),
|
||||||
|
(4118, 'Scaffold Carsentry', 571, 8127.2900, -380.4440, 1001.7900, 1),
|
||||||
|
(4119, 'Scroll of Recall', 0, -103.9880, -902.7950, 55.5340, 1),
|
||||||
|
(4120, 'Scroll of Recall', 0, -10446.9000, -3261.9100, 20.1790, 1),
|
||||||
|
(4121, 'Scroll of Recall', 0, -5506.3400, -704.3480, 392.6860, 1),
|
||||||
|
(4122, 'Scroll of Recall', 0, -9470.7600, 3.9090, 49.7940, 1),
|
||||||
|
(4123, 'Scroll of Recall', 0, 1804.8400, 196.3220, 70.3990, 1),
|
||||||
|
(4124, 'Scroll of Recall', 0, 286.3140, -2184.0900, 122.6120, 1),
|
||||||
|
(4125, 'Scroll of Recall', 1, -1060.2700, 23.1370, 141.4550, 1),
|
||||||
|
(4126, 'Scroll of Recall', 1, -506.2240, -2590.0800, 113.1500, 1),
|
||||||
|
(4127, 'Scroll of Recall', 1, -7135.7200, -3787.7700, 8.7990, 1),
|
||||||
|
(4128, 'Scroll of Recall', 1, 6395.7100, 433.2560, 33.2600, 1),
|
||||||
|
(4129, 'Scryers Tier innkeeper', 530, -2184.0400, 5399.6200, 51.9658, 1),
|
||||||
|
(4130, 'Searing Gorge Blackchar Cave', 0, -7249.7500, -814.5230, 296.6230, 1),
|
||||||
|
(4131, 'Searing Gorge Blackrock Mountain', 0, -7243.0600, -1122.9400, 274.6300, 1),
|
||||||
|
(4132, 'Searing Gorge The Slag Pit', 0, -6601.6300, -1250.7500, 188.0640, 1),
|
||||||
|
(4133, 'Sewer Teleport 01', 571, 5881.2000, 666.5000, 615.7940, 1),
|
||||||
|
(4134, 'Sewer Teleport 04', 571, 5815.3000, 714.6000, 619.0980, 1),
|
||||||
|
(4135, 'Shadow Port', 33, -103.4600, 2122.1000, 155.6550, 1),
|
||||||
|
(4136, 'Shadow Port', 33, -105.6540, 2154.9800, 156.4300, 1),
|
||||||
|
(4137, 'Shadow Port', 33, -84.9900, 2151.0100, 155.6200, 1),
|
||||||
|
(4138, 'Shadowblink', 469, -7469.9300, -1227.9300, 476.7770, 1),
|
||||||
|
(4139, 'Shadowblink', 469, -7486.3600, -1194.3200, 476.8000, 1),
|
||||||
|
(4140, 'Shadowblink', 469, -7500.7000, -1249.8900, 476.7980, 1),
|
||||||
|
(4141, 'Shadowblink', 469, -7506.5800, -1165.2600, 476.7960, 1),
|
||||||
|
(4142, 'Shadowblink', 469, -7524.3600, -1219.1200, 476.7940, 1),
|
||||||
|
(4143, 'Shadowblink', 469, -7538.6300, -1273.6400, 476.8000, 1),
|
||||||
|
(4144, 'Shadowblink', 469, -7542.4700, -1191.9200, 476.3550, 1),
|
||||||
|
(4145, 'Shadowblink', 469, -7561.5400, -1244.0100, 476.8000, 1),
|
||||||
|
(4146, 'Shadowblink', 469, -7581.1100, -1216.1900, 476.8000, 1),
|
||||||
|
(4147, 'Shadowmoon Darkcaster', 540, 72.4708, 184.4520, -13.2380, 1),
|
||||||
|
(4148, 'Shadowmoon Valley Oronoks Farm', 530, -2802.3200, 1286.4600, 77.3836, 1),
|
||||||
|
(4149, 'Shattrath Banish Teleport', 530, -1500.0300, 5217.1400, 32.4600, 1),
|
||||||
|
(4150, 'Shattrath City Aldor Rise', 530, -1751.2400, 5641.2300, 129.0710, 1),
|
||||||
|
(4151, 'Shattrath City Scryers Tier', 530, -2041.2900, 5561.9700, 54.4371, 1),
|
||||||
|
(4152, 'Shattrath City innkeeper', 530, -1903.7900, 5765.7000, 131.2960, 1),
|
||||||
|
(4153, 'Shattrath Portal to Darnassus', 530, -1790.9800, 5413.9800, -12.4282, 1),
|
||||||
|
(4154, 'Shattrath Portal to Exodar', 530, -1880.2800, 5357.5300, -12.4281, 1),
|
||||||
|
(4155, 'Shattrath Portal to Exodar', 530, -4031.2400, -11569.6000, -138.2990, 1),
|
||||||
|
(4156, 'Shattrath Portal to Ironforge', 530, -1795.7900, 5399.6300, -12.4281, 1),
|
||||||
|
(4157, 'Shattrath Portal to Orgrimmar', 530, -1934.4900, 5453.4800, -12.4279, 1),
|
||||||
|
(4158, 'Shattrath Portal to Silvermoon', 530, -1894.6900, 5362.3400, -12.4282, 1),
|
||||||
|
(4159, 'Shattrath Portal to Stormwind', 0, -8998.1400, 861.2540, 29.6206, 1),
|
||||||
|
(4160, 'Shattrath Portal to Stormwind', 530, -1792.7800, 5406.5400, -12.4279, 1),
|
||||||
|
(4161, 'Shattrath Portal to Thunder Bluff', 530, -1936.3200, 5445.9500, -12.4282, 1),
|
||||||
|
(4162, 'Shattrath Portal to Undercity', 0, 1773.4200, 61.7391, -46.3215, 1),
|
||||||
|
(4163, 'Shattrath Portal to Undercity', 530, -1931.4800, 5460.4900, -12.4281, 1),
|
||||||
|
(4164, 'Ship (The Bravery)dock', 0, -8644.6400, 1333.7100, 5.7993, 1),
|
||||||
|
(4165, 'Ship (The Bravery)dock', 1, 6419.1900, 818.6660, 6.1894, 1),
|
||||||
|
(4166, 'Ship (The Lady Mehley)dock', 0, -3898.2700, -597.7260, 5.5310, 1),
|
||||||
|
(4167, 'Ship (The Lady Mehley)dock', 1, -4006.9700, -4730.3300, 5.5366, 1),
|
||||||
|
(4168, 'Ship (The Maidens Fancy)dock', 0, -14279.5000, 571.2000, 6.0777, 1),
|
||||||
|
(4169, 'Ship (The Maidens Fancy)dock', 1, -997.3520, -3830.2500, 5.5803, 1),
|
||||||
|
(4170, 'Ship, Icebreaker (Northspear)dock', 571, 584.0140, -5098.6700, -6.4857, 1),
|
||||||
|
(4171, 'Ship, Night Elf (Elunes Blessing)dock', 1, 6546.9800, 927.7040, 6.1894, 1),
|
||||||
|
(4172, 'Ship, Night Elf (Elunes Blessing)dock', 530, -4264.1400, -11328.3000, 6.0035, 1),
|
||||||
|
(4173, 'Ship, Night Elf (Feathermoon Ferry)dock', 1, -4210.9100, 3280.7300, 5.7750, 1),
|
||||||
|
(4174, 'Ship, Night Elf (Moonspray)dock', 1, 6584.1600, 773.2750, 6.0986, 1),
|
||||||
|
(4175, 'Ship, Night Elf (Moonspray)dock', 1, 8545.0900, 1016.1700, 6.2912, 1),
|
||||||
|
(4176, 'Sholazar Basin Nesingwary Base Camp', 571, 5559.1100, 5765.4300, -77.9385, 1),
|
||||||
|
(4177, 'Sholazar Basin The Seabreach Flow', 571, 5527.8500, 5348.4900, -134.4620, 1),
|
||||||
|
(4178, 'Silithus Ortells Hideout', 1, -7578.7200, 196.9140, 11.5488, 1),
|
||||||
|
(4179, 'Silithus Twilights Run', 1, -6265.9300, 41.8866, 9.0625, 1),
|
||||||
|
(4180, 'Silverpine Forest Fenris Keep', 0, 993.8510, 690.0600, 74.8984, 1),
|
||||||
|
(4181, 'Sister Mercydock', 571, 254.1330, -3752.8000, 0.2291, 1),
|
||||||
|
(4182, 'Sister Mercydock', 571, 93.8904, -3681.4100, 0.2103, 1),
|
||||||
|
(4183, 'Skadi Teleport', 575, 476.7990, -511.1670, 104.7230, 1),
|
||||||
|
(4184, 'Skarthis the Summoner', 547, -76.9917, -157.0810, -2.1064, 1),
|
||||||
|
(4185, 'Socrethar Return Portal Effect', 530, 4778.4600, 3455.3600, 104.1300, 1),
|
||||||
|
(4186, 'Stonetalon Mountains Boulderslide Ravine', 1, -97.6734, 185.5910, 96.6757, 1),
|
||||||
|
(4187, 'Stonetalon Mountains Cragpool Lake', 1, 1605.9900, 96.7067, 98.5654, 1),
|
||||||
|
(4188, 'Stonetalon Mountains Windshear Mine', 1, 935.9090, -302.7410, 0.0220, 1),
|
||||||
|
(4189, 'Storm Peaks', 571, 8974.3700, -1280.5600, 1059.0100, 1),
|
||||||
|
(4190, 'Stormwind Stockade', 34, 131.1490, 3.3395, -25.5229, 1),
|
||||||
|
(4191, 'Stormwind to Dustwallow Teleport', 1, -3722.9100, -4413.9600, 26.1300, 1),
|
||||||
|
(4192, 'Stoutlager Inn innkeeper', 0, -5377.9100, -2973.9100, 323.2520, 1),
|
||||||
|
(4193, 'Stranglethorn Vale Kurzens Compound', 0, -11571.3000, -644.0530, 31.2554, 1),
|
||||||
|
(4194, 'Stranglethorn Vale The Vile Reef', 0, -12211.5000, 644.0560, -67.1350, 1),
|
||||||
|
(4195, 'Subwayentry', 369, -1.0668, -11.3475, -3.9157, 1),
|
||||||
|
(4196, 'Subwayentry', 369, -1.0668, 2470.3000, -3.9157, 1),
|
||||||
|
(4197, 'Subwayentry', 369, -39.7335, 9.7886, -3.9157, 1),
|
||||||
|
(4198, 'Subwayentry', 369, -39.7335, -10.3899, -3.9157, 1),
|
||||||
|
(4199, 'Subwayentry', 369, -39.7335, 30.3816, -3.9157, 1),
|
||||||
|
(4200, 'Subwayentry', 369, -50.9334, 2472.9300, -3.9157, 1),
|
||||||
|
(4201, 'Subwayentry', 369, -50.9334, 2512.1500, -3.9157, 1),
|
||||||
|
(4202, 'Subwayentry', 369, -51.0097, 2492.2300, -3.9157, 1),
|
||||||
|
(4203, 'Subwayentry', 369, 10.1333, 2510.4900, -3.9157, 1),
|
||||||
|
(4204, 'Subwayentry', 369, 10.1333, 2490.7400, -3.9157, 1),
|
||||||
|
(4205, 'Subwayentry', 369, 10.1333, 8.8000, -3.9157, 1),
|
||||||
|
(4206, 'Subwayentry', 369, 10.1896, 28.7706, -3.9157, 1),
|
||||||
|
(4207, 'Summon Menagerie', 578, 1116.1100, 1075.1700, 508.3490, 1),
|
||||||
|
(4208, 'Summon Menagerie', 578, 1163.7200, 1170.9900, 527.3220, 1),
|
||||||
|
(4209, 'Summon Menagerie', 578, 968.7080, 1042.4900, 527.3220, 1),
|
||||||
|
(4210, 'Sunken Temple', 109, -455.3780, 76.5158, -93.3827, 1),
|
||||||
|
(4211, 'Surface Portal', 571, 5795.8800, 2070.8700, -344.0460, 1),
|
||||||
|
(4212, 'Surface Portal', 571, 6203.8700, 2262.1700, 497.1970, 1),
|
||||||
|
(4213, 'Surge Needle Teleporter', 571, 3444.3800, 2361.8700, 38.7409, 1),
|
||||||
|
(4214, 'Swamp of Sorrows Stagalbog Cave', 0, -10919.3000, -3673.0500, 11.0245, 1),
|
||||||
|
(4215, 'Swamp of Sorrows Stonard', 0, -10437.8000, -3307.0400, 20.4499, 1),
|
||||||
|
(4216, 'Teldrassil Banethil Barrow Den', 1, 9794.8400, 1549.1500, 1262.8000, 1),
|
||||||
|
(4217, 'Teldrassil Fel Rock', 1, 10124.0000, 1115.9100, 1322.8200, 1),
|
||||||
|
(4218, 'Teldrassil Shadowthread Cave', 1, 10889.4000, 917.5860, 1326.6400, 1),
|
||||||
|
(4219, 'Teleport - Coldarra, Transitus Shield to Amber Ledge', 571, 3594.1800, 5997.5100, 136.2150, 1),
|
||||||
|
(4220, 'Teleport - Dalaran to Wintergrasp', 571, 5325.0600, 2843.3600, 409.2850, 1),
|
||||||
|
(4221, 'Teleport Darkshire', 0, -10566.0000, -1189.0000, 28.0000, 1),
|
||||||
|
(4222, 'Teleport Defenders', 607, 1226.4300, -71.9616, 70.0842, 1),
|
||||||
|
(4223, 'Teleport Duskwood', 0, -10368.0000, -422.0000, 64.1214, 1),
|
||||||
|
(4224, 'Teleport Elwynn', 0, -9104.0000, -70.0000, 83.0000, 1),
|
||||||
|
(4225, 'Teleport Goldshire', 0, -9464.0000, 62.0000, 56.0000, 1),
|
||||||
|
(4226, 'Teleport Left', 533, 2692.0000, -3399.2700, 267.6860, 1),
|
||||||
|
(4227, 'Teleport Moonbrook', 0, -11020.0000, 1436.0000, 43.6892, 1),
|
||||||
|
(4228, 'Teleport Players on Victory', 631, -548.9830, 2211.2400, 539.2900, 1),
|
||||||
|
(4229, 'Teleport Return', 533, 2685.0600, -3502.3700, 261.3150, 1),
|
||||||
|
(4230, 'Teleport Right', 533, 2692.0000, -3321.8600, 267.6860, 1),
|
||||||
|
(4231, 'Teleport Sanctum Moon - Down', 530, 7513.6300, -6388.9300, 23.8000, 1),
|
||||||
|
(4232, 'Teleport Sanctum Sun - Down', 530, 7199.4000, -7097.3600, 66.9700, 1),
|
||||||
|
(4233, 'Teleport To Zone In', 616, 728.0550, 1329.0300, 275.0000, 1),
|
||||||
|
(4234, 'Teleport Violet Citadel Spire Down', 571, 5790.0000, 734.0000, 640.0000, 1),
|
||||||
|
(4235, 'Teleport Westfall', 0, -10643.0000, 1052.0000, 33.8437, 1),
|
||||||
|
(4236, 'Teleport and Transform', 580, 1667.6400, 633.4660, 28.0500, 1),
|
||||||
|
(4237, 'Teleport back to Main Room', 603, 1970.6100, -25.5988, 324.5500, 1),
|
||||||
|
(4238, 'Teleport from Azshara Tower', 1, 3641.0000, -4702.0000, 121.0000, 1),
|
||||||
|
(4239, 'Teleport to Ashtongue NPCs', 564, 702.2200, 200.3000, 125.0100, 1),
|
||||||
|
(4240, 'Teleport to Center', 568, -34.3160, 1149.6400, 19.1550, 1),
|
||||||
|
(4241, 'Teleport to Chamber Illusion', 603, 2043.1200, -25.6981, 239.7210, 1),
|
||||||
|
(4242, 'Teleport to CoT Stratholme Phase 4', 595, 2071.5500, 1287.6800, 141.6870, 1),
|
||||||
|
(4243, 'Teleport to Council', 564, 603.4200, 305.9820, 271.9000, 1),
|
||||||
|
(4244, 'Teleport to Final Chamber Effect DND', 531, -8632.8400, 2055.8700, 108.8600, 1),
|
||||||
|
(4245, 'Teleport to Gnomeregan', 0, -5095.0000, 757.0000, 261.0000, 1),
|
||||||
|
(4246, 'Teleport to Hall of Command', 0, 2402.6200, -5633.2800, 377.0210, 1),
|
||||||
|
(4247, 'Teleport to Heart of Acherus', 609, 2419.9100, -5620.4800, 420.6440, 1),
|
||||||
|
(4248, 'Teleport to Icecrown Illusion', 603, 1949.1300, -80.6744, 239.9900, 1),
|
||||||
|
(4249, 'Teleport to Inside Violet Hold', 608, 1857.2400, 803.8770, 44.0085, 1),
|
||||||
|
(4250, 'Teleport to Lake Wintergrasp', 571, 4561.5800, 2835.3300, 389.7900, 1),
|
||||||
|
(4251, 'Teleport to Lake Wintergrasp', 571, 5025.7100, 3673.4100, 362.6870, 1),
|
||||||
|
(4252, 'Teleport to Lake Wintergrasp', 571, 5094.6700, 2170.3300, 365.6010, 1),
|
||||||
|
(4253, 'Teleport to Lake Wintergrasp', 571, 5386.0500, 2840.9700, 418.6750, 1),
|
||||||
|
(4254, 'Teleport to Molten Core DND', 409, 1080.0000, -483.0000, -108.0000, 1),
|
||||||
|
(4255, 'Teleport to Stormwind Illusion', 603, 1954.1400, 21.5220, 239.7180, 1),
|
||||||
|
(4256, 'Teleport to Sunwell Plateau', 580, 1861.4500, 495.1250, 82.9059, 1),
|
||||||
|
(4257, 'Teleport to Twin Emps Effect DND', 531, -8971.8100, 1321.4700, -104.2490, 1),
|
||||||
|
(4258, 'Teleport to Violet Stand', 571, 5724.6200, 1013.1700, 174.4800, 1),
|
||||||
|
(4259, 'Teleport to the Silvermoon', 530, 10021.1000, -7014.8700, 49.7100, 1),
|
||||||
|
(4260, 'Teleport to the Undercity', 0, 1805.9300, 335.6600, 70.3900, 1),
|
||||||
|
(4261, 'Teleport', 1, -3891.8000, -4609.9700, 9.5011, 1),
|
||||||
|
(4262, 'Teleport', 409, 736.5160, -1176.3500, -119.0060, 1),
|
||||||
|
(4263, 'Teleport', 531, -8306.6800, 2060.8400, 133.0620, 1),
|
||||||
|
(4264, 'Teleport', 531, -8330.6300, 2123.1400, 133.0620, 1),
|
||||||
|
(4265, 'Teleport', 533, 2633.4900, -3529.5600, 274.1110, 1),
|
||||||
|
(4266, 'Teleport', 533, 2905.6300, -3769.9600, 273.6200, 1),
|
||||||
|
(4267, 'Teleport', 571, 3574.2200, 6652.1300, 195.1850, 1),
|
||||||
|
(4268, 'Teleport', 571, 3646.7400, 5893.2000, 174.4830, 1),
|
||||||
|
(4269, 'Teleport', 571, 4590.9400, -5711.2400, 184.5070, 1),
|
||||||
|
(4270, 'Teleport', 576, 504.7420, 88.9122, -16.1245, 1),
|
||||||
|
(4271, 'Teleport', 578, 1103.4700, 1049.5700, 512.0000, 1),
|
||||||
|
(4272, 'Teleport', 600, -369.0000, -601.0000, 2.0000, 1),
|
||||||
|
(4273, 'Teleport: Argent Tournament', 571, 8480.5500, 1092.9000, 554.4850, 1),
|
||||||
|
(4274, 'Teleport: Black Temple', 530, -3560.5200, 583.3530, 10.9431, 1),
|
||||||
|
(4275, 'Teleport: Darnassus', 1, 9664.1400, 2526.3600, 1332.6900, 1),
|
||||||
|
(4276, 'Teleport: Moonglade', 1, 7992.6200, -2680.0400, 512.0990, 1),
|
||||||
|
(4277, 'Teleport: Shattrath', 530, -1842.0700, 5497.1700, -12.4306, 1),
|
||||||
|
(4278, 'Teleport: Stonard', 0, -10469.0000, -3331.5400, 25.4716, 1),
|
||||||
|
(4279, 'Teleport: Theramore', 1, -3748.1100, -4440.2100, 30.5688, 1),
|
||||||
|
(4280, 'Tempest Keep', 550, 411.4090, -39.8267, 20.1802, 1);
|
||||||
|
INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES
|
||||||
|
(4281, 'Tempest Keep: The Arcatraz', 552, 278.6480, -12.6903, 22.4479, 1),
|
||||||
|
(4282, 'Tempest Keep: The Botanica', 553, 17.5470, 404.8610, -27.1645, 1),
|
||||||
|
(4283, 'Tempest Keep: The Mechanar', 554, 169.2420, -12.2941, -0.0010, 1),
|
||||||
|
(4284, 'Terokkar Forest Skettis', 530, -3944.9300, 3664.0900, 287.9900, 1),
|
||||||
|
(4285, 'Terokkar Forest Terokks Rest', 530, -3868.3500, 3521.8800, 278.5610, 1),
|
||||||
|
(4286, 'Test of Lore', 1, -2354.0300, -1902.0700, 95.7800, 1),
|
||||||
|
(4287, 'The Barrens Baeldun Keep', 1, -4071.7300, -2381.6400, 126.2140, 1),
|
||||||
|
(4288, 'The Barrens Dreadmist Den', 1, 318.8870, -2225.7300, 212.5040, 1),
|
||||||
|
(4289, 'The Chilled Quagmire spiritguide', 571, 5103.1300, 3462.1300, 368.5680, 1),
|
||||||
|
(4290, 'The Chilled Quagmire spirithealer', 571, 5099.0300, 3469.6700, 368.4850, 1),
|
||||||
|
(4291, 'The Nexus', 576, 545.9580, -125.9330, -24.9367, 1),
|
||||||
|
(4292, 'The Skybreaker', 623, 5.2403, 0.2579, 20.8691, 1),
|
||||||
|
(4293, 'The Storm Peaks Frostfloe Deep', 571, 8300.4300, -2564.8600, 1153.5900, 1),
|
||||||
|
(4294, 'The Storm Peaks Frostgrips Hollow', 571, 6956.1400, -2.5143, 808.5300, 1),
|
||||||
|
(4295, 'The Storm Peaks Gimoraks Den', 571, 7920.2400, -1625.2100, 910.8520, 1),
|
||||||
|
(4296, 'The Storm Peaks Hibernal Cavern', 571, 7241.0300, -2075.5900, 763.0780, 1),
|
||||||
|
(4297, 'The Storm Peaks Plain of Echoes', 571, 8109.8900, -2815.7900, 1135.3200, 1),
|
||||||
|
(4298, 'The Storm Peaks The Forlorn Mine', 571, 6929.4000, -1315.7700, 831.3460, 1),
|
||||||
|
(4299, 'The Storm Peaks The Frozen Mine', 571, 7768.5000, -16.3215, 864.4010, 1),
|
||||||
|
(4300, 'The Storm Peaks', 571, 7268.5100, -2125.3800, 778.1580, 1),
|
||||||
|
(4301, 'The Sunken Ring spiritguide', 571, 5104.7500, 2300.9500, 368.5680, 1),
|
||||||
|
(4302, 'The Zephyrdock', 1, -1026.8600, 358.4000, 133.3400, 1),
|
||||||
|
(4303, 'The Zephyrdock', 1, 1139.7300, -4142.9300, 52.0080, 1),
|
||||||
|
(4304, 'Thousand Needles Darkcloud Pinnacle', 1, -4904.6700, -1970.1300, 86.8811, 1),
|
||||||
|
(4305, 'Thousand Needles Roguefeather Den', 1, -5533.0400, -1602.8800, 29.1719, 1),
|
||||||
|
(4306, 'Thousand Needles Splithoof Hold', 1, -4952.9400, -2337.3000, -56.5321, 1),
|
||||||
|
(4307, 'Thousand Needles The Weathered Nook', 1, -5217.6500, -2788.9900, -7.4459, 1),
|
||||||
|
(4308, 'Thrall', 0, 1953.8400, 233.8350, 41.8800, 1),
|
||||||
|
(4309, 'Thrall', 1, 1920.0100, -4123.9500, 43.6300, 1),
|
||||||
|
(4310, 'Thunderbrew Distillery innkeeper', 0, -5601.6000, -531.2030, 399.7370, 1),
|
||||||
|
(4311, 'Tirisfal Glades Agamand Family Crypt', 0, 3043.6500, 681.8670, 67.0126, 1),
|
||||||
|
(4312, 'Tirisfal Glades Gallows End Tavern', 0, 2262.2600, 244.2570, 33.7170, 1),
|
||||||
|
(4313, 'To Icecrown Airship - Player - Aura - Teleport to Dalaran Trigger', 571, 5831.5300, 497.0880, 657.4660, 1),
|
||||||
|
(4314, 'Toshleys Station Transporter', 530, 2054.0500, 5569.1600, 263.5710, 1),
|
||||||
|
(4315, 'Translocate', 0, 1805.0000, 327.0000, 70.5000, 1),
|
||||||
|
(4316, 'Translocate', 530, -2259.7400, 3215.0300, -4.0500, 1),
|
||||||
|
(4317, 'Translocate', 530, -2307.3500, 3123.9200, 13.6900, 1),
|
||||||
|
(4318, 'Translocate', 530, -589.0000, 4079.0000, 143.3000, 1),
|
||||||
|
(4319, 'Translocate', 530, 12780.9000, -6877.5000, 22.7861, 1),
|
||||||
|
(4320, 'Translocate', 530, 9334.5000, -7880.7600, 74.9094, 1),
|
||||||
|
(4321, 'Translocation', 530, -594.0000, 4079.0000, 94.0000, 1),
|
||||||
|
(4322, 'Trespasser!', 571, 5773.0000, 703.5000, 641.6000, 1),
|
||||||
|
(4323, 'Trespasser!', 571, 5846.5000, 605.5000, 650.9000, 1),
|
||||||
|
(4324, 'Trespasser!', 571, 8460.0000, 700.0000, 547.4000, 1),
|
||||||
|
(4325, 'Trespasser!', 571, 8573.0000, 703.9000, 547.3000, 1),
|
||||||
|
(4326, 'Turtle (Green Island)dock', 571, 2649.6000, 844.8000, 1.7773, 1),
|
||||||
|
(4327, 'Turtle (Walker of Waves)dock', 571, 2638.1300, 938.4000, 1.7773, 1),
|
||||||
|
(4328, 'Uldaman', 70, -42.3058, 263.8750, -48.9355, 1),
|
||||||
|
(4329, 'Ulduar spirithealer', 571, 9025.6600, -1178.5700, 1060.0800, 1),
|
||||||
|
(4330, 'Undervatorentry', 0, 1552.8000, 240.7730, 55.4904, 1),
|
||||||
|
(4331, 'Undervatorentry', 0, 1596.1500, 283.2000, 55.4904, 1),
|
||||||
|
(4332, 'Undervatorentry', 0, 1596.2000, 197.1040, 55.4904, 1),
|
||||||
|
(4333, 'Undervatorentryentry', 0, 1564.0000, 240.6560, 55.7571, 1),
|
||||||
|
(4334, 'Undervatorentryentry', 0, 1595.3800, 197.7060, 55.4904, 1),
|
||||||
|
(4335, 'Undervatorentryentry', 0, 1595.3800, 213.3330, 57.8905, 1),
|
||||||
|
(4336, 'Undervatorentryentry', 0, 1595.6500, 271.8890, 55.7571, 1),
|
||||||
|
(4337, 'Use Legion Teleporter', 530, -2833.0900, 1949.8900, 201.2560, 1),
|
||||||
|
(4338, 'Vampiric Shadowbat', 532, -10930.9000, -1995.7500, 49.4768, 1),
|
||||||
|
(4339, 'Vanish', 309, -11516.1000, -1605.3100, 41.3000, 1),
|
||||||
|
(4340, 'Vator2entry', 90, -800.3290, 314.9250, -272.4900, 1),
|
||||||
|
(4341, 'Vatorentry', 0, -5163.7700, 655.3050, 348.5880, 1),
|
||||||
|
(4342, 'Vatorentry', 0, -5164.2400, 650.3540, 247.9780, 1),
|
||||||
|
(4343, 'Vortex', 616, 755.0000, 1301.0000, 280.0000, 1),
|
||||||
|
(4344, 'Watery Grave', 548, 337.6900, -732.8700, -13.7400, 1),
|
||||||
|
(4345, 'Watery Grave', 548, 365.5300, -737.1200, -14.0000, 1),
|
||||||
|
(4346, 'Watery Grave', 548, 366.2700, -709.4000, -13.9200, 1),
|
||||||
|
(4347, 'Watery Grave', 548, 372.8500, -690.8400, -13.9100, 1),
|
||||||
|
(4348, 'Weegli Blastfuse', 209, 1881.0500, 1297.3600, 48.4190, 1),
|
||||||
|
(4349, 'Western Plaguelands Mardenholde Keep', 0, 2936.4100, -1395.9000, 166.0270, 1),
|
||||||
|
(4350, 'Westfall The Cooper Residence', 0, -11023.1000, 1547.4800, 44.4936, 1),
|
||||||
|
(4351, 'Winterspring Everlook', 1, 6768.3400, -4668.5200, 723.7480, 1),
|
||||||
|
(4352, 'Winterspring Moon Horror Den', 1, 7124.2700, -4637.7800, 639.6550, 1),
|
||||||
|
(4353, 'Wyrmrest Temple flightMaster', 571, 3647.2600, 244.0510, 52.3397, 1),
|
||||||
|
(4354, 'Wyrmrest Temple spirithealer', 571, 3546.8600, 272.9880, 45.5817, 1),
|
||||||
|
(4355, 'Zeppelin (The Iron Eagle)dock', 0, -12452.0000, 221.0670, 31.7681, 1),
|
||||||
|
(4356, 'Zeppelin (The Iron Eagle)dock', 1, 1359.5700, -4632.2000, 53.6569, 1),
|
||||||
|
(4357, 'Zeppelin (The Purple Princess)dock', 0, -12407.5000, 211.8380, 31.5014, 1),
|
||||||
|
(4358, 'Zeppelin (The Purple Princess)dock', 0, 2061.4200, 235.5580, 100.1520, 1),
|
||||||
|
(4359, 'Zeppelin (The Thundercaller)dock', 0, 2064.2400, 291.6030, 97.0904, 1),
|
||||||
|
(4360, 'Zeppelin (The Thundercaller)dock', 1, 1319.3200, -4658.0900, 53.8358, 1),
|
||||||
|
(4361, 'Zeppelindock', 571, 1412.9900, -3092.5800, 166.2270, 1),
|
||||||
|
(4362, 'ZulDrak Amphitheater of Anguish', 571, 5715.7900, -2945.0100, 296.5510, 1),
|
||||||
|
(4363, 'ZulDrak ZimTorga', 571, 5756.5800, -3567.6300, 387.0390, 1),
|
||||||
|
(4364, '[PH] Teleport to Auberdine', 1, 6581.0500, 767.5000, 5.7843, 1),
|
||||||
|
(4365, '[PH] Teleport to Booty Bay', 0, -14457.0000, 496.4500, 39.1392, 1),
|
||||||
|
(4366, '[PH] Teleport to Felwood', 1, 5483.9000, -749.8810, 334.6210, 1),
|
||||||
|
(4367, '[PH] Teleport to GromGol', 0, -12415.0000, 207.6180, 31.5017, 1),
|
||||||
|
(4368, '[PH] Teleport to Menethil Harbor', 0, -3752.8100, -851.5580, 10.1153, 1),
|
||||||
|
(4369, '[PH] Teleport to Orgrimmar', 1, 1552.5000, -4420.6600, 8.9480, 1),
|
||||||
|
(4370, '[PH] Teleport to Theramore', 1, -3615.4900, -4467.3400, 21.6032, 1),
|
||||||
|
(4371, 'c-Maraudon', 1, -1424.0400, 2945.0100, 134.5400, 1),
|
||||||
|
(4372, 'c-Tauren start', 1, -3034.7000, 144.0400, 70.8700, 1),
|
||||||
|
(4373, 'c1-Blackfathom Deeps', 1, 4158.0100, 877.6000, -20.6800, 1),
|
||||||
|
(4374, 'c1-Blackrock Mountain', 0, -7502.2000, -1152.9800, 269.5500, 1),
|
||||||
|
(4375, 'c1-Coilfang', 530, 571.1000, 6938.9700, -16.8100, 1),
|
||||||
|
(4376, 'c1-Deadmine exit', 0, -11367.5000, 1617.1000, 71.2200, 1),
|
||||||
|
(4377, 'c1-Dire Maul', 1, -3626.3900, 917.3700, 150.1300, 1),
|
||||||
|
(4378, 'c1-Ebon Hold', 609, 2390.0200, -5640.9100, 377.0900, 1),
|
||||||
|
(4379, 'c1-Sunken Temple', 0, -10416.5000, -3832.5300, -36.9200, 1),
|
||||||
|
(4380, 'c1-The Noxious Pass', 609, 2528.2200, -5580.4400, 162.0200, 1),
|
||||||
|
(4381, 'c1-Timbermaw Hold', 1, 7016.7500, -2153.8400, 595.0900, 1),
|
||||||
|
(4382, 'c1-Wailing Caverns', 1, -588.5300, -2037.6900, 57.6000, 1),
|
||||||
|
(4383, 'c2-Blackfathom Deeps', 1, 4156.6000, 909.8900, -20.9700, 1),
|
||||||
|
(4384, 'c2-Blackrock Mountain', 0, -7591.3100, -1114.4400, 249.9100, 1),
|
||||||
|
(4385, 'c2-Coilfang', 530, 571.1000, 6938.9700, -15.2000, 1),
|
||||||
|
(4386, 'c2-Deadmine exit', 0, -11367.1000, 1610.4800, 76.6300, 1),
|
||||||
|
(4387, 'c2-Dire Maul', 1, -3628.0800, 919.5500, 137.8400, 1),
|
||||||
|
(4388, 'c2-Ebon Hold', 609, 2383.6500, -5645.2000, 420.7700, 1),
|
||||||
|
(4389, 'c2-Sunken Temple', 0, -10408.7000, -3834.2900, -44.6900, 1),
|
||||||
|
(4390, 'c2-The Noxious Pass', 609, 2538.3500, -5573.0500, 162.4600, 1),
|
||||||
|
(4391, 'c3-Blackfathom Deeps', 1, 4157.4600, 916.4400, -17.4000, 1),
|
||||||
|
(4392, 'c3-Coilfang', 530, 651.0700, 6865.3700, -82.3400, 1),
|
||||||
|
(4393, 'c3-Deadmine exit', 0, -11381.5000, 1584.1100, 82.1000, 1),
|
||||||
|
(4394, 'c3-Ebon Hold', 609, 2325.0300, -5659.6000, 382.2400, 1),
|
||||||
|
(4395, 'c3-Scarlet Enclave', 609, 2409.0900, -5722.3700, 154.0000, 1),
|
||||||
|
(4396, 'c3-Sunken Temple', 0, -10325.2000, -3865.8600, -44.4500, 1),
|
||||||
|
(4397, 'c3-The Noxious Pass', 609, 2546.6100, -5563.8900, 162.8800, 1),
|
||||||
|
(4398, 'c4-Coilfang', 530, 607.1700, 6908.6800, -49.2000, 1),
|
||||||
|
(4399, 'c4-Deadmine exit', 0, -11379.6000, 1578.7900, 87.8400, 1),
|
||||||
|
(4400, 'c4-Ebon Hold', 609, 2348.5800, -5695.3500, 382.2400, 1),
|
||||||
|
(4401, 'c4-Scarlet Enclave', 609, 2402.8600, -5727.0300, 154.0000, 1),
|
||||||
|
(4402, 'c4-The Noxious Pass', 609, 2563.5200, -5547.7900, 163.2700, 1),
|
||||||
|
(4403, 'c5-Coilfang', 530, 574.6800, 6942.9300, -37.7200, 1),
|
||||||
|
(4404, 'c5-Deadmine exit', 0, -11340.2000, 1571.6100, 94.4400, 1),
|
||||||
|
(4405, 'c6-Coilfang', 530, 723.7400, 6865.7800, -74.1000, 1),
|
||||||
|
(4406, 'c7-Coilfang', 530, 731.5700, 6866.0100, -70.4700, 1);
|
||||||
File diff suppressed because it is too large
Load Diff
143899
data/sql/playerbots/updates/2026_05_09_03_travelnode_path_cmangos_import.sql
Normal file
143899
data/sql/playerbots/updates/2026_05_09_03_travelnode_path_cmangos_import.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
|||||||
|
-- Manual travelnode coverage for the Aldrassil ramp in Shadowglen
|
||||||
|
-- (Teldrassil, map 1, zone 141). Adds 9 anchor nodes along the spiral
|
||||||
|
-- ramp (base -> intermediate ramp waypoints -> top platform near
|
||||||
|
-- Tenaron Stormgrip). All nodes are `linked = 0` so
|
||||||
|
-- `.playerbots travel generatenode` will iterate them and let mmap
|
||||||
|
-- compute the actual walk paths between consecutive nodes. Splitting
|
||||||
|
-- the climb into short segments (~30y each) gives mmap a much better
|
||||||
|
-- chance of resolving each piece than a single 300y end-to-end probe.
|
||||||
|
--
|
||||||
|
-- TEMPORARILY DISABLED: isolating generatenode behaviour on the OG
|
||||||
|
-- (cmangos-imported) graph. Re-enable by removing the comment prefix
|
||||||
|
-- once the regen flow is verified stable.
|
||||||
|
|
||||||
|
-- SET @n1 := (SELECT IFNULL(MAX(id), 0) + 1 FROM playerbots_travelnode);
|
||||||
|
-- SET @n2 := @n1 + 1;
|
||||||
|
-- SET @n3 := @n1 + 2;
|
||||||
|
-- SET @n4 := @n1 + 3;
|
||||||
|
-- SET @n5 := @n1 + 4;
|
||||||
|
-- SET @n6 := @n1 + 5;
|
||||||
|
-- SET @n7 := @n1 + 6;
|
||||||
|
-- SET @n8 := @n1 + 7;
|
||||||
|
-- SET @n9 := @n1 + 8;
|
||||||
|
|
||||||
|
-- INSERT INTO playerbots_travelnode (id, name, map_id, x, y, z, linked) VALUES
|
||||||
|
-- (@n1, 'Aldrassil Ramp 1 (base)', 1, 10413.756, 887.97363, 1319.3668, 0),
|
||||||
|
-- (@n2, 'Aldrassil Ramp 2', 1, 10440.520, 870.32320, 1328.9324, 0),
|
||||||
|
-- (@n3, 'Aldrassil Ramp 3', 1, 10497.001, 854.46014, 1345.1770, 0),
|
||||||
|
-- (@n4, 'Aldrassil Ramp 4', 1, 10517.199, 821.48640, 1354.7914, 0),
|
||||||
|
-- (@n5, 'Aldrassil Ramp 5', 1, 10477.926, 847.88855, 1372.1685, 0),
|
||||||
|
-- (@n6, 'Aldrassil Ramp 6', 1, 10455.358, 831.34240, 1380.9377, 0),
|
||||||
|
-- (@n7, 'Aldrassil Ramp 7', 1, 10460.220, 800.71716, 1388.3368, 0),
|
||||||
|
-- (@n8, 'Aldrassil Ramp 8', 1, 10507.434, 793.30420, 1397.2166, 0),
|
||||||
|
-- (@n9, 'Aldrassil Ramp 9 (top)', 1, 10495.496, 804.67700, 1397.2662, 0);
|
||||||
|
|
||||||
|
SELECT 1; -- no-op so the file is still valid SQL for the updater
|
||||||
351
extract_ac_client_data.sh
Normal file
351
extract_ac_client_data.sh
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# AzerothCore client-data extraction for mod-playerbots.
|
||||||
|
# Run from anywhere — output lands in $SERVER_DATA_DIR.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── PATHS ──────────────────────────────────────────────────────────────────
|
||||||
|
WOW_CLIENT_DATA="/home/dev/wow_client_data"
|
||||||
|
TOOLS_DIR="/home/dev/azerothcore_installer/_server/azerothcore/env/dist/bin"
|
||||||
|
SERVER_DATA_DIR="$TOOLS_DIR"
|
||||||
|
|
||||||
|
# ─── TOGGLES ────────────────────────────────────────────────────────────────
|
||||||
|
EXTRACT_DBC_AND_MAPS=true
|
||||||
|
EXTRACT_VMAPS=true
|
||||||
|
EXTRACT_MMAPS=true
|
||||||
|
|
||||||
|
MMAP_THREADS=0 # 0 = auto-detect (each thread uses 1-2 GB RAM)
|
||||||
|
MMAP_SINGLE_MAP="" # e.g. "489" for Warsong Gulch only
|
||||||
|
|
||||||
|
# Verbatim copy of azerothcore-wotlk/master:src/tools/mmaps_generator/mmaps-config.yaml
|
||||||
|
# (also the same config the mod-playerbots fork ships).
|
||||||
|
MMAPS_CONFIG_YAML=$(cat <<'YAML_EOF'
|
||||||
|
mmapsConfig:
|
||||||
|
skipLiquid: false
|
||||||
|
skipContinents: false
|
||||||
|
skipJunkMaps: true
|
||||||
|
skipBattlegrounds: false
|
||||||
|
|
||||||
|
# Path to the directory containing navigation data files.
|
||||||
|
# This directory should contain the "maps" and "vmaps" folders,
|
||||||
|
# and is also where the "mmaps" folder will be created or located.
|
||||||
|
dataDir: "./"
|
||||||
|
|
||||||
|
meshSettings:
|
||||||
|
# Here we have global config for recast navigation.
|
||||||
|
# It's possible to override these data on map or tile level (see mapsOverrides).
|
||||||
|
|
||||||
|
# Maximum slope angle (in degrees) NPCs can walk on.
|
||||||
|
# Surfaces steeper than this will be considered unwalkable.
|
||||||
|
walkableSlopeAngle: 50
|
||||||
|
|
||||||
|
# --- Cell Size Calculation ---
|
||||||
|
# Many parameters below are defined in "cell units".
|
||||||
|
# In RecastDemo, you often work with world units instead of cell units.
|
||||||
|
# The actual generator uses (src/tools/mmaps_generator/Config.cpp:28):
|
||||||
|
#
|
||||||
|
# cellSize = MMAP::GRID_SIZE / vertexPerMapEdge
|
||||||
|
#
|
||||||
|
# Where:
|
||||||
|
# MMAP::GRID_SIZE = 533.3333f (the size of one map tile in world units)
|
||||||
|
# vertexPerMapEdge = number of vertices along one edge of the full map grid
|
||||||
|
#
|
||||||
|
# Example (AC stock):
|
||||||
|
# vertexPerMapEdge = 2000 → cellSize ≈ 533.3333 / 2000 ≈ 0.2667 yd
|
||||||
|
#
|
||||||
|
# IMPORTANT: when changing vertexPerMapEdge, the per-cell parameters
|
||||||
|
# below (walkableHeight, walkableClimb, walkableRadius) must be re-scaled
|
||||||
|
# to preserve their world-unit semantics. Doubling vertexPerMapEdge
|
||||||
|
# halves cellSize, so cell counts must double to keep the same yd value.
|
||||||
|
#
|
||||||
|
# To convert a value from cell units to world units (e.g., walkableClimb),
|
||||||
|
# multiply by cellSize. For example, a walkableClimb of 6 at 2000 resolution:
|
||||||
|
# 6 × 0.2667 ≈ 1.60 yd
|
||||||
|
|
||||||
|
# Minimum ceiling height (in cell units) NPCs need to pass under an obstacle.
|
||||||
|
# Controls how much vertical clearance is required.
|
||||||
|
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
|
||||||
|
# 6 cells × 0.2667 yd ≈ 1.60 yd — matches WoW player capsule height at
|
||||||
|
# 2000 resolution (AC stock). Preserves the 1.60 yd world-unit
|
||||||
|
# ceiling-clearance requirement.
|
||||||
|
walkableHeight: 6
|
||||||
|
|
||||||
|
# Maximum height difference (in cell units) NPCs can step up or down.
|
||||||
|
# Higher values allow walking over fences, ledges, or steps.
|
||||||
|
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
|
||||||
|
#
|
||||||
|
# Vanilla WotLK uses 6, which allows creatures to "jump" over fences.
|
||||||
|
# Classic WotLK uses 4, which forces creatures to walk around fences.
|
||||||
|
# 6 cells × 0.2667 yd ≈ 1.60 yd — Vanilla-WotLK step semantics at
|
||||||
|
# 2000 resolution. Preserves the 1.60 yd world-unit step. The mmap
|
||||||
|
# is shared with every creature, NPC patrol, escort, and quest mob;
|
||||||
|
# tightening below stock breaks patrols that cross 1.5y ledges.
|
||||||
|
walkableClimb: 4
|
||||||
|
|
||||||
|
# Minimum distance (in cell units) around walkable surfaces.
|
||||||
|
# Helps prevent NPCs from clipping into walls and narrow gaps.
|
||||||
|
# To convert to world units, multiply by cellSize (see "Cell Size Calculation").
|
||||||
|
# 2 cells × 0.2667 yd ≈ 0.53 yd — AC stock world-unit buffer at
|
||||||
|
# 2000 resolution. Tested wider (= 0.71y world units): erodes polys
|
||||||
|
# near mountains/cliffs so pathfinder routes through surviving
|
||||||
|
# (higher/worse) polys → bot climbs mountains.
|
||||||
|
walkableRadius: 2
|
||||||
|
|
||||||
|
# Number of vertices along one edge of the entire map's navmesh grid.
|
||||||
|
# Higher values increase mesh resolution but also CPU/memory usage.
|
||||||
|
# 2000 = AC stock baseline. cellSize ≈ 0.2667 yd.
|
||||||
|
vertexPerMapEdge: 2000
|
||||||
|
|
||||||
|
# Number of vertices along one edge of each tile chunk.
|
||||||
|
# Must divide vertexPerMapEdge evenly — the generator uses integer
|
||||||
|
# division: tilesPerMapEdge = vertexPerMap / vertexPerTile
|
||||||
|
# (src/tools/mmaps_generator/Config.cpp:144).
|
||||||
|
# A higher vertex count per tile means fewer total tiles,
|
||||||
|
# reducing runtime work to load, unload, and manage tiles.
|
||||||
|
# 80 = AC stock baseline. 2000 / 80 = 25 tiles per map edge, 625
|
||||||
|
# tiles per map (~21y per tile). Lots of small tiles, low per-tile
|
||||||
|
# RAM, more seams to stitch across.
|
||||||
|
vertexPerTileEdge: 80
|
||||||
|
|
||||||
|
# Tolerance for how much a polygon can deviate from the original geometry when simplified.
|
||||||
|
# Higher values produce simpler (faster) meshes but can reduce accuracy.
|
||||||
|
# 0.8 (vs the AC stock 1.8 and recast canonical 1.3) keeps polygon
|
||||||
|
# edges close to real terrain. Targets "merged step into ramp"
|
||||||
|
# simplification artifacts that produce corner-cuts and false NOPATH.
|
||||||
|
maxSimplificationError: 0.8
|
||||||
|
|
||||||
|
# You can override any global parameter for a specific map by specifying its map ID.
|
||||||
|
# Inside each map override, you can also override parameters per individual tile,
|
||||||
|
# identified by a string "tileX,tileY" (coordinates).
|
||||||
|
#
|
||||||
|
# Overrides cascade: global settings → map overrides → tile overrides.
|
||||||
|
# For example:
|
||||||
|
#
|
||||||
|
# mapsOverrides:
|
||||||
|
# "0": # Map ID 0 overrides
|
||||||
|
# walkableRadius: 5 # Override global climb height for entire map 0
|
||||||
|
#
|
||||||
|
# tilesOverrides:
|
||||||
|
# "50,70": # Tile at coordinates (50,70) on map 0
|
||||||
|
# walkableSlopeAngle: 70 # Override slope angle locally just here
|
||||||
|
# walkableClimb: 4 # Also override climb height for this tile only
|
||||||
|
#
|
||||||
|
# "51,71":
|
||||||
|
# walkableClimb: 3 # Override climb height for tile (51,71)
|
||||||
|
#
|
||||||
|
# "48,32":
|
||||||
|
# walkableClimb: 1 # Even smaller climb for tile (48,32)
|
||||||
|
#
|
||||||
|
# "1": # Map ID 1 overrides example
|
||||||
|
# walkableHeight: 8 # Increase clearance for whole map 1
|
||||||
|
#
|
||||||
|
# tilesOverrides:
|
||||||
|
# "100,100":
|
||||||
|
# maxSimplificationError: 2.5 # Looser mesh simplification for this tile only
|
||||||
|
#
|
||||||
|
# "101,101":
|
||||||
|
# walkableRadius: 1 # Smaller NPC radius here for tight corridors
|
||||||
|
#
|
||||||
|
# This approach allows very fine-grained control of navigation mesh parameters
|
||||||
|
# on a per-map and per-tile basis, optimizing pathfinding quality and performance.
|
||||||
|
#
|
||||||
|
# All parameters defined globally are eligible for override.
|
||||||
|
# Just specify the parameter name and new value in the override section.
|
||||||
|
mapsOverrides:
|
||||||
|
"562": # Blade's Edge Arena
|
||||||
|
walkableRadius: 0 # This allows walking on the ropes to the pillars
|
||||||
|
|
||||||
|
"48": # Blackfathom Deeps
|
||||||
|
cellSizeVertical: 0.5334 # ch*2 = 0.2667 * 2 ≈ 0.5334. Reduce the chance to have underground levels.
|
||||||
|
|
||||||
|
"529": # Arathi Basin
|
||||||
|
tilesOverrides:
|
||||||
|
"30,29": # Lumber Mill
|
||||||
|
# Make sure that Fear will not drop players rom cliff -
|
||||||
|
# https://github.com/azerothcore/azerothcore-wotlk/pull/22462#issuecomment-3067024680
|
||||||
|
walkableSlopeAngle: 45
|
||||||
|
|
||||||
|
"530": # Outland
|
||||||
|
tilesOverrides:
|
||||||
|
"32,30": # Dark portal
|
||||||
|
walkableSlopeAngle: 45 # https://github.com/chromiecraft/chromiecraft/issues/8404#issuecomment-3476012660
|
||||||
|
|
||||||
|
# debugOutput generates debug files in the `meshes` directory for use with RecastDemo.
|
||||||
|
# This is useful for inspecting and debugging mmap generation visually.
|
||||||
|
#
|
||||||
|
# My workflow:
|
||||||
|
# 1. Install RecastDemo. I'm building it from the source of this fork: https://github.com/jackpoz/recastnavigation
|
||||||
|
# 2. In-game, move your character to the area you want to debug.
|
||||||
|
# 3. Type `.mmap loc` in chat. This will output:
|
||||||
|
# - The current tile file name (e.g., `04832.mmtile`)
|
||||||
|
# - The Recast config values used to generate that tile
|
||||||
|
# 4. Enable `debugOutput` and regenerate mmaps (preferably just the tile from step 3).
|
||||||
|
# - To regenerate only one tile, delete it from the `mmaps` folder.
|
||||||
|
# 5. After generation, you will find debug files in the `meshes` folder, including an OBJ file (e.g., `map0004832.obj`)
|
||||||
|
# 6. Copy these debug files to the `Meshes` folder used by RecastDemo.
|
||||||
|
# - RecastDemo expects this folder to be in the same directory as its executable.
|
||||||
|
# 7. In RecastDemo:
|
||||||
|
# - Click "Input Mesh" and select the `.obj` file
|
||||||
|
# - Choose "Solo Mesh" in the Sample selector
|
||||||
|
# 8. (Optional) Reuse the Recast config values from step 3:
|
||||||
|
# - `cellSizeHorizontal` → "Cell Size"
|
||||||
|
# - `walkableSlopeAngle` → "Max Slope"
|
||||||
|
# - `walkableClimb` → "Max Climb"
|
||||||
|
# - and so on
|
||||||
|
# 9. Scroll to the bottom of RecastDemo UI and press "Build" to generate the navigation mesh
|
||||||
|
debugOutput: false
|
||||||
|
YAML_EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ─── DO NOT EDIT BELOW ──────────────────────────────────────────────────────
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
[ -n "$SERVER_DATA_DIR" ] || { echo "SERVER_DATA_DIR is not set"; exit 1; }
|
||||||
|
[ -n "$WOW_CLIENT_DATA" ] || { echo "WOW_CLIENT_DATA is not set"; exit 1; }
|
||||||
|
mkdir -p "$SERVER_DATA_DIR"
|
||||||
|
cd "$SERVER_DATA_DIR"
|
||||||
|
|
||||||
|
# ─── SAFETY: source MPQs are READ-ONLY to this script ──────────────────────
|
||||||
|
# Resolve both paths to canonical form and refuse to run if the output dir
|
||||||
|
# is inside the source. Combined with safe_rm() below, this script cannot
|
||||||
|
# touch any file inside WOW_CLIENT_DATA.
|
||||||
|
SERVER_DATA_DIR_REAL="$(cd "$SERVER_DATA_DIR" && pwd -P)"
|
||||||
|
WOW_CLIENT_DATA_REAL="$(cd "$WOW_CLIENT_DATA" && pwd -P 2>/dev/null || echo "$WOW_CLIENT_DATA")"
|
||||||
|
case "$SERVER_DATA_DIR_REAL/" in
|
||||||
|
"$WOW_CLIENT_DATA_REAL"/|"$WOW_CLIENT_DATA_REAL"/*)
|
||||||
|
echo "ERROR: SERVER_DATA_DIR ($SERVER_DATA_DIR_REAL) is inside WOW_CLIENT_DATA — refusing." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Refuses to remove anything outside SERVER_DATA_DIR. Resolves the parent
|
||||||
|
# to absolute path so a symlink inside cwd can't trick us into traversing
|
||||||
|
# into the source. Use this for every cleanup in this script.
|
||||||
|
safe_rm() {
|
||||||
|
local target="$1"
|
||||||
|
local parent_abs base
|
||||||
|
parent_abs="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)" || return 0
|
||||||
|
base="$(basename -- "$target")"
|
||||||
|
local abs="$parent_abs/$base"
|
||||||
|
case "$abs/" in
|
||||||
|
"$SERVER_DATA_DIR_REAL"/|"$SERVER_DATA_DIR_REAL"/*) ;;
|
||||||
|
*)
|
||||||
|
echo "REFUSING to rm path outside SERVER_DATA_DIR: $target → $abs" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
rm -rf -- "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$MMAP_THREADS" -eq 0 ] && MMAP_THREADS=$(nproc 2>/dev/null || echo 4)
|
||||||
|
|
||||||
|
echo "Working dir : $(pwd)"
|
||||||
|
echo "Tools dir : $TOOLS_DIR"
|
||||||
|
echo "Threads : $MMAP_THREADS"
|
||||||
|
echo "Steps : maps=$EXTRACT_DBC_AND_MAPS vmaps=$EXTRACT_VMAPS mmaps=$EXTRACT_MMAPS"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ─── Symlink Data/ → MPQ source (only when extracting from client) ──────────
|
||||||
|
if [ "$EXTRACT_DBC_AND_MAPS" = true ] || [ "$EXTRACT_VMAPS" = true ]; then
|
||||||
|
has_mpqs() { find "$1" -maxdepth 1 -iname "*.mpq" -print -quit 2>/dev/null | grep -q .; }
|
||||||
|
|
||||||
|
if has_mpqs "$WOW_CLIENT_DATA"; then
|
||||||
|
MPQ_DIR="$WOW_CLIENT_DATA"
|
||||||
|
elif has_mpqs "$WOW_CLIENT_DATA/Data"; then
|
||||||
|
MPQ_DIR="$WOW_CLIENT_DATA/Data"
|
||||||
|
else
|
||||||
|
echo "ERROR: no .mpq files in $WOW_CLIENT_DATA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MPQ_DIR="$(cd "$MPQ_DIR" && pwd)"
|
||||||
|
|
||||||
|
# Symlink only — refuse to clobber an existing real directory.
|
||||||
|
if [ -e Data ] && [ ! -L Data ]; then
|
||||||
|
echo "ERROR: Data/ exists in $(pwd) but is not a symlink" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ln -sfn "$MPQ_DIR" Data
|
||||||
|
echo "Data/ → $MPQ_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── STEP 1: DBCs + Maps ────────────────────────────────────────────────────
|
||||||
|
if [ "$EXTRACT_DBC_AND_MAPS" = true ]; then
|
||||||
|
echo
|
||||||
|
echo "[1/3] Extracting DBCs + Maps..."
|
||||||
|
# Clean slate — map_extractor refuses to run if these dirs already exist.
|
||||||
|
safe_rm dbc
|
||||||
|
safe_rm maps
|
||||||
|
safe_rm Cameras
|
||||||
|
# -e 7 = bitfield MAP(1)|DBC(2)|CAMERA(4) — extract everything.
|
||||||
|
# The old "-e 2" was DBC-only and skipped maps + cameras entirely.
|
||||||
|
"$TOOLS_DIR/map_extractor" -e 7 -f 0
|
||||||
|
if [ ! -d maps ] || [ -z "$(ls -A maps 2>/dev/null)" ]; then
|
||||||
|
echo "ERROR: map_extractor finished but maps/ is empty — check its output above" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── STEP 2: VMaps ──────────────────────────────────────────────────────────
|
||||||
|
if [ "$EXTRACT_VMAPS" = true ]; then
|
||||||
|
echo
|
||||||
|
echo "[2/3] Extracting VMaps..."
|
||||||
|
# Clean slate — vmap4_extractor refuses to run if these dirs already exist.
|
||||||
|
safe_rm Buildings
|
||||||
|
safe_rm vmaps
|
||||||
|
"$TOOLS_DIR/vmap4_extractor" -l -d ./Data
|
||||||
|
mkdir -p vmaps
|
||||||
|
"$TOOLS_DIR/vmap4_assembler" Buildings vmaps
|
||||||
|
safe_rm Buildings
|
||||||
|
if [ ! -d vmaps ] || [ -z "$(ls -A vmaps 2>/dev/null)" ]; then
|
||||||
|
echo "ERROR: vmap4_assembler finished but vmaps/ is empty — check output above" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── STEP 3: MMaps ──────────────────────────────────────────────────────────
|
||||||
|
if [ "$EXTRACT_MMAPS" = true ]; then
|
||||||
|
if [ ! -d maps ]; then
|
||||||
|
echo "ERROR: maps/ missing in $(pwd) — run with EXTRACT_DBC_AND_MAPS=true once" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -d vmaps ]; then
|
||||||
|
echo "ERROR: vmaps/ missing in $(pwd) — run with EXTRACT_VMAPS=true once" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[3/3] Generating MMaps... (do not interrupt)"
|
||||||
|
printf '%s\n' "$MMAPS_CONFIG_YAML" > mmaps-config.yaml
|
||||||
|
|
||||||
|
# Wipe any existing tiles before regenerating. Mixed tiles from
|
||||||
|
# previous runs (different cellSize / verticesPerTileEdge / etc.)
|
||||||
|
# would otherwise be silently kept and mixed with new ones,
|
||||||
|
# producing a corrupt navmesh. Clean slate every mmap run.
|
||||||
|
safe_rm mmaps
|
||||||
|
mkdir -p mmaps
|
||||||
|
|
||||||
|
# Workaround: some mmaps_generator builds write a few tiles to /mmaps
|
||||||
|
# via an absolute path. Pre-create it so the writes don't fail, then
|
||||||
|
# fold the strays into our local mmaps/ at the end.
|
||||||
|
sudo rm -rf /mmaps
|
||||||
|
sudo mkdir -p /mmaps && sudo chmod 777 /mmaps
|
||||||
|
|
||||||
|
CMD=("$TOOLS_DIR/mmaps_generator" --config mmaps-config.yaml --threads "$MMAP_THREADS")
|
||||||
|
[ -n "$MMAP_SINGLE_MAP" ] && CMD+=("$MMAP_SINGLE_MAP")
|
||||||
|
|
||||||
|
START=$(date +%s)
|
||||||
|
"${CMD[@]}"
|
||||||
|
ELAPSED=$(( $(date +%s) - START ))
|
||||||
|
|
||||||
|
if compgen -G "/mmaps/*.mmtile" >/dev/null; then
|
||||||
|
cp /mmaps/*.mmtile mmaps/ && rm -f /mmaps/*.mmtile
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "MMap done in $((ELAPSED / 60))m $((ELAPSED % 60))s"
|
||||||
|
echo "Tiles: $(ls mmaps/*.mmtile 2>/dev/null | wc -l)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done. Restart worldserver to pick up changes."
|
||||||
@ -173,7 +173,7 @@ std::vector<uint32> const vFlagsIC = {GO_HORDE_BANNER,
|
|||||||
GO_HORDE_BANNER_GRAVEYARD_H,
|
GO_HORDE_BANNER_GRAVEYARD_H,
|
||||||
GO_HORDE_BANNER_GRAVEYARD_H_CONT};
|
GO_HORDE_BANNER_GRAVEYARD_H_CONT};
|
||||||
|
|
||||||
// BG Waypoints (vmangos)
|
// BG Waypoints
|
||||||
|
|
||||||
// Horde Flag Room to Horde Graveyard
|
// Horde Flag Room to Horde Graveyard
|
||||||
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {
|
BattleBotPath vPath_WSG_HordeFlagRoom_to_HordeGraveyard = {
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
#include "CheckValuesAction.h"
|
#include "CheckValuesAction.h"
|
||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
#include "ObjectGuid.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
|
|
||||||
#include "PlayerbotAI.h"
|
#include "PlayerbotAI.h"
|
||||||
#include "TravelNode.h"
|
|
||||||
#include "AiObjectContext.h"
|
#include "AiObjectContext.h"
|
||||||
|
|
||||||
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {}
|
||||||
@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/)
|
|||||||
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
botAI->Ping(bot->GetPositionX(), bot->GetPositionY());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT))
|
|
||||||
{
|
|
||||||
TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT));
|
|
||||||
}
|
|
||||||
|
|
||||||
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
GuidVector possible_targets = *context->GetValue<GuidVector>("possible targets");
|
||||||
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
GuidVector all_targets = *context->GetValue<GuidVector>("all targets");
|
||||||
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
GuidVector npcs = *context->GetValue<GuidVector>("nearest npcs");
|
||||||
|
|||||||
@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::vector<WorldPosition> beginPath, endPath;
|
std::vector<WorldPosition> beginPath, endPath;
|
||||||
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
|
TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Traveling to " << dest->getTitle() << ": ";
|
out << "Traveling to " << dest->getTitle() << ": ";
|
||||||
@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
WorldPosition pos(bot);
|
WorldPosition pos(bot);
|
||||||
|
|
||||||
std::string const name = "USER:" + text.substr(9);
|
std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName();
|
||||||
|
std::string const name = "USER:" + suffix;
|
||||||
|
|
||||||
/* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal.
|
{
|
||||||
|
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
|
||||||
|
TravelNodeMap::instance().addNode(pos, name, false, true);
|
||||||
|
|
||||||
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
|
for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000))
|
||||||
{
|
|
||||||
endNode->setLinked(false);
|
endNode->setLinked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMasterNoFacing("Node " + name + " created.");
|
botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes.");
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event)
|
|||||||
if (startNode->isImportant())
|
if (startNode->isImportant())
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Node can not be removed.");
|
botAI->TellMasterNoFacing("Node can not be removed.");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNodeMap::instance().m_nMapMtx.lock();
|
{
|
||||||
|
std::lock_guard<std::shared_timed_mutex> lock(TravelNodeMap::instance().m_nMapMtx);
|
||||||
TravelNodeMap::instance().removeNode(startNode);
|
TravelNodeMap::instance().removeNode(startNode);
|
||||||
botAI->TellMasterNoFacing("Node removed.");
|
}
|
||||||
TravelNodeMap::instance().m_nMapMtx.unlock();
|
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes.");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event)
|
|||||||
node->removeLinkTo(path.first, true);
|
node->removeLinkTo(path.first, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("gen node") != std::string::npos)
|
else if (text.find("gen node") != std::string::npos ||
|
||||||
|
text.find("gen path") != std::string::npos)
|
||||||
{
|
{
|
||||||
// Pathfinder
|
// Disabled: generateAll() touches Map / grid / mmap state that is only
|
||||||
TravelNodeMap::instance().generateNodes();
|
// safe to mutate on the world thread. Running it from a detached worker
|
||||||
return true;
|
// (or from a bot tick on a MapUpdater thread) races with world updates
|
||||||
}
|
// and freezes the server. Use the console command instead, which runs
|
||||||
else if (text.find("gen path") != std::string::npos)
|
// synchronously on the world thread:
|
||||||
{
|
// .playerbots travel generatenode
|
||||||
TravelNodeMap::instance().generatePaths();
|
botAI->TellMasterNoFacing(
|
||||||
|
"Disabled in chat. Run '.playerbots travel generatenode' from the server console.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (text.find("crop path") != std::string::npos)
|
else if (text.find("crop path") != std::string::npos)
|
||||||
@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
[]
|
[]
|
||||||
{
|
{
|
||||||
TravelNodeMap::instance().removeNodes();
|
TravelNodeMap::instance().removeNodes();
|
||||||
TravelNodeMap::instance().loadNodeStore();
|
TravelNodeMap::instance().LoadNodeStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.detach();
|
t.detach();
|
||||||
@ -297,7 +300,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
|
|
||||||
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
// uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal.
|
||||||
|
|
||||||
std::vector<WorldPosition> ppath = l.second->getPath();
|
std::vector<WorldPosition> ppath = l.second->GetPath();
|
||||||
|
|
||||||
for (auto p : ppath)
|
for (auto p : ppath)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,6 +29,10 @@ void DestroyItemAction::DestroyItem(FindItemVisitor* visitor)
|
|||||||
std::vector<Item*> items = visitor->GetResult();
|
std::vector<Item*> items = visitor->GetResult();
|
||||||
for (Item* item : items)
|
for (Item* item : items)
|
||||||
{
|
{
|
||||||
|
// backstop: never drop an active quest item
|
||||||
|
if (bot->HasQuestForItem(item->GetTemplate()->ItemId))
|
||||||
|
continue;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
|
out << chat->FormatItem(item->GetTemplate()) << " destroyed";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
@ -67,18 +71,11 @@ bool SmartDestroyItemAction::Execute(Event /*event*/)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ITEM_USAGE_QUEST is excluded — those are still-needed quest items
|
||||||
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
|
std::vector<uint32> bestToDestroy = {ITEM_USAGE_NONE}; // First destroy anything useless.
|
||||||
|
|
||||||
if (!AI_VALUE(bool, "can sell") &&
|
|
||||||
AI_VALUE(
|
|
||||||
bool,
|
|
||||||
"should get money")) // We need money so quest items are less important since they can't directly be sold.
|
|
||||||
bestToDestroy.push_back(ITEM_USAGE_QUEST);
|
|
||||||
else // We don't need money so destroy the cheapest stuff.
|
|
||||||
{
|
|
||||||
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
|
bestToDestroy.push_back(ITEM_USAGE_VENDOR);
|
||||||
bestToDestroy.push_back(ITEM_USAGE_AH);
|
bestToDestroy.push_back(ITEM_USAGE_AH);
|
||||||
}
|
|
||||||
|
|
||||||
// If we still need room
|
// If we still need room
|
||||||
bestToDestroy.push_back(
|
bestToDestroy.push_back(
|
||||||
|
|||||||
@ -19,83 +19,8 @@
|
|||||||
#include "Transport.h"
|
#include "Transport.h"
|
||||||
#include "Map.h"
|
#include "Map.h"
|
||||||
|
|
||||||
namespace
|
// Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport,
|
||||||
{
|
// BoardTransport) are now on MovementAction — inherited by FollowAction.
|
||||||
Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
|
|
||||||
{
|
|
||||||
if (!map || !ref)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
|
|
||||||
for (float const pz : probes)
|
|
||||||
{
|
|
||||||
if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref))
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to find a point on the leader's transport that is closer to the bot,
|
|
||||||
// by probing along the segment from master -> bot and returning the last point
|
|
||||||
// that is still detected as being on the expected transport.
|
|
||||||
bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
|
|
||||||
float masterX, float masterY, float masterZ,
|
|
||||||
float botX, float botY, float botZ,
|
|
||||||
float& outX, float& outY, float& outZ)
|
|
||||||
{
|
|
||||||
if (!map || !expectedTransport || !ref)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
uint32 const phaseMask = ref->GetPhaseMask();
|
|
||||||
|
|
||||||
// Ensure master is actually detected on that transport (tolerant).
|
|
||||||
if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z.
|
|
||||||
float const probeZ = std::max(masterZ, botZ);
|
|
||||||
|
|
||||||
// Adaptive step count: small platforms need tighter sampling.
|
|
||||||
float const dx2 = botX - masterX;
|
|
||||||
float const dy2 = botY - masterY;
|
|
||||||
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
|
|
||||||
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
|
|
||||||
|
|
||||||
float const dx = (botX - masterX) / static_cast<float>(steps);
|
|
||||||
float const dy = (botY - masterY) / static_cast<float>(steps);
|
|
||||||
|
|
||||||
// Master must actually be on the expected transport for this to work.
|
|
||||||
if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float lastX = masterX;
|
|
||||||
float lastY = masterY;
|
|
||||||
bool found = false;
|
|
||||||
|
|
||||||
for (int32 i = 1; i <= steps; ++i)
|
|
||||||
{
|
|
||||||
float const px = masterX + dx * i;
|
|
||||||
float const py = masterY + dy * i;
|
|
||||||
|
|
||||||
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
|
|
||||||
if (t != expectedTransport)
|
|
||||||
break;
|
|
||||||
|
|
||||||
lastX = px;
|
|
||||||
lastY = py;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
outX = lastX;
|
|
||||||
outY = lastY;
|
|
||||||
outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FollowAction::Execute(Event /*event*/)
|
bool FollowAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
@ -170,9 +95,8 @@ bool FollowAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
bool const movingAllowed = IsMovingAllowed();
|
bool const movingAllowed = IsMovingAllowed();
|
||||||
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
|
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
|
||||||
bool const waiting = IsWaitingForLastMove(priority);
|
|
||||||
|
|
||||||
if (movingAllowed && !dupMove && !waiting)
|
if (movingAllowed && !dupMove)
|
||||||
{
|
{
|
||||||
if (bot->IsSitState())
|
if (bot->IsSitState())
|
||||||
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
bot->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
|||||||
@ -24,9 +24,7 @@ using ai::buff::MakeAuraQualifierForBuff;
|
|||||||
using ai::spell::HasSpellOrCategoryCooldown;
|
using ai::spell::HasSpellOrCategoryCooldown;
|
||||||
|
|
||||||
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
|
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
|
||||||
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell)
|
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CastSpellAction::Execute(Event /*event*/)
|
bool CastSpellAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
@ -53,18 +51,12 @@ bool CastSpellAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
wstrToLower(wnamepart);
|
wstrToLower(wnamepart);
|
||||||
|
|
||||||
if (!Utf8FitTo(spell, wnamepart))
|
if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
uint32 itemId = spellInfo->Effects[0].ItemType;
|
uint32 itemId = spellInfo->Effects[0].ItemType;
|
||||||
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
|
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
|
||||||
if (!proto)
|
if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (spellInfo->Id > castId)
|
if (spellInfo->Id > castId)
|
||||||
@ -92,10 +84,7 @@ bool CastSpellAction::isUseful()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Unit* spellTarget = GetTarget();
|
Unit* spellTarget = GetTarget();
|
||||||
if (!spellTarget)
|
if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// float combatReach = bot->GetCombatReach() + target->GetCombatReach();
|
// float combatReach = bot->GetCombatReach() + target->GetCombatReach();
|
||||||
@ -143,10 +132,7 @@ CastMeleeSpellAction::CastMeleeSpellAction(
|
|||||||
bool CastMeleeSpellAction::isUseful()
|
bool CastMeleeSpellAction::isUseful()
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target || !bot->IsWithinMeleeRange(target))
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!bot->IsWithinMeleeRange(target))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return CastSpellAction::isUseful();
|
return CastSpellAction::isUseful();
|
||||||
@ -162,10 +148,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
|
|||||||
bool CastMeleeDebuffSpellAction::isUseful()
|
bool CastMeleeDebuffSpellAction::isUseful()
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target || !bot->IsWithinMeleeRange(target))
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!bot->IsWithinMeleeRange(target))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return CastDebuffSpellAction::isUseful();
|
return CastDebuffSpellAction::isUseful();
|
||||||
@ -175,14 +158,55 @@ bool CastAuraSpellAction::isUseful()
|
|||||||
{
|
{
|
||||||
if (!GetTarget() || !CastSpellAction::isUseful())
|
if (!GetTarget() || !CastSpellAction::isUseful())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
|
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
|
||||||
if (!aura)
|
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
|
||||||
return true;
|
|
||||||
if (beforeDuration && aura->GetDuration() < beforeDuration)
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CastBuffSpellAction::isUseful()
|
||||||
|
{
|
||||||
|
Unit* target = GetTarget();
|
||||||
|
if (!target || !CastSpellAction::isUseful())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
|
||||||
|
return !aura || (beforeDuration && aura->GetDuration() < beforeDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CastBuffSpellAction::Execute(Event /*event*/)
|
||||||
|
{
|
||||||
|
return botAI->CastSpell(spell, GetTarget());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GroupBuffSpellAction::isUseful()
|
||||||
|
{
|
||||||
|
Unit* target = GetTarget();
|
||||||
|
if (!target || !CastSpellAction::isUseful())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ai::buff::IsGroupVariantEnabled(bot, spell))
|
||||||
|
{
|
||||||
|
std::string const groupVariant = ai::buff::GroupVariantFor(spell);
|
||||||
|
if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
|
||||||
|
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GroupBuffSpellAction::Execute(Event /*event*/)
|
||||||
|
{
|
||||||
|
std::string const castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, spell);
|
||||||
|
return botAI->CastSpell(castName, GetTarget());
|
||||||
|
}
|
||||||
|
|
||||||
CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
|
CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
|
||||||
PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
|
PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
|
||||||
|
|
||||||
@ -248,25 +272,16 @@ Value<Unit*>* CurePartyMemberAction::GetTargetValue()
|
|||||||
return context->GetValue<Unit*>("party member to dispel", dispelType);
|
return context->GetValue<Unit*>("party member to dispel", dispelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make Bots Paladin, druid, mage use the greater buff rank spell
|
|
||||||
// TODO Priest doen't verify il he have components
|
|
||||||
Value<Unit*>* BuffOnPartyAction::GetTargetValue()
|
Value<Unit*>* BuffOnPartyAction::GetTargetValue()
|
||||||
|
{
|
||||||
|
return context->GetValue<Unit*>("party member without aura", spell);
|
||||||
|
}
|
||||||
|
|
||||||
|
Value<Unit*>* GroupBuffOnPartyAction::GetTargetValue()
|
||||||
{
|
{
|
||||||
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
|
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BuffOnPartyAction::Execute(Event /*event*/)
|
|
||||||
{
|
|
||||||
std::string castName = spell; // default = mono
|
|
||||||
|
|
||||||
auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(
|
|
||||||
bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
|
|
||||||
|
|
||||||
return botAI->CastSpell(castName, GetTarget());
|
|
||||||
}
|
|
||||||
// End greater buff fix
|
|
||||||
|
|
||||||
CastShootAction::CastShootAction(
|
CastShootAction::CastShootAction(
|
||||||
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
|
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
|
||||||
{
|
{
|
||||||
@ -365,16 +380,7 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
|
|||||||
bool CastEveryManForHimselfAction::isPossible()
|
bool CastEveryManForHimselfAction::isPossible()
|
||||||
{
|
{
|
||||||
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
|
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
|
||||||
if (!spellId)
|
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!bot->HasSpell(spellId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (HasSpellOrCategoryCooldown(bot, spellId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CastEveryManForHimselfAction::isUseful()
|
bool CastEveryManForHimselfAction::isUseful()
|
||||||
@ -390,16 +396,7 @@ bool CastEveryManForHimselfAction::isUseful()
|
|||||||
bool CastWillOfTheForsakenAction::isPossible()
|
bool CastWillOfTheForsakenAction::isPossible()
|
||||||
{
|
{
|
||||||
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
|
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
|
||||||
if (!spellId)
|
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!bot->HasSpell(spellId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (HasSpellOrCategoryCooldown(bot, spellId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CastWillOfTheForsakenAction::isUseful()
|
bool CastWillOfTheForsakenAction::isUseful()
|
||||||
@ -427,10 +424,7 @@ bool UseTrinketAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
bool UseTrinketAction::UseTrinket(Item* item)
|
bool UseTrinketAction::UseTrinket(Item* item)
|
||||||
{
|
{
|
||||||
if (bot->CanUseItem(item) != EQUIP_ERR_OK)
|
if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true))
|
||||||
return false;
|
|
||||||
|
|
||||||
if (bot->IsNonMeleeSpellCast(true))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint8 bagIndex = item->GetBagSlot();
|
uint8 bagIndex = item->GetBagSlot();
|
||||||
@ -477,14 +471,13 @@ bool UseTrinketAction::UseTrinket(Item* item)
|
|||||||
if (spellProcFlag != 0) return false;
|
if (spellProcFlag != 0) return false;
|
||||||
|
|
||||||
if (!botAI->CanCastSpell(spellId, bot, false))
|
if (!botAI->CanCastSpell(spellId, bot, false))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!spellId)
|
if (!spellId)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
WorldPacket packet(CMSG_USE_ITEM);
|
WorldPacket packet(CMSG_USE_ITEM);
|
||||||
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
|
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
|
||||||
|
|
||||||
@ -500,9 +493,8 @@ bool CastDebuffSpellAction::isUseful()
|
|||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target || !target->IsAlive() || !target->IsInWorld())
|
if (!target || !target->IsAlive() || !target->IsInWorld())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return CastAuraSpellAction::isUseful() &&
|
return CastAuraSpellAction::isUseful() &&
|
||||||
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
|
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,9 +69,7 @@ class CastDebuffSpellAction : public CastAuraSpellAction
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f)
|
CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f)
|
||||||
: CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime)
|
: CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
bool isUseful() override;
|
bool isUseful() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -90,9 +88,7 @@ class CastDebuffSpellOnAttackerAction : public CastDebuffSpellAction
|
|||||||
public:
|
public:
|
||||||
CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
|
CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
|
||||||
float needLifeTime = 8.0f)
|
float needLifeTime = 8.0f)
|
||||||
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
|
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
std::string const getName() override { return spell + " on attacker"; }
|
std::string const getName() override { return spell + " on attacker"; }
|
||||||
@ -104,9 +100,7 @@ class CastDebuffSpellOnMeleeAttackerAction : public CastDebuffSpellAction
|
|||||||
public:
|
public:
|
||||||
CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
|
CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
|
||||||
float needLifeTime = 8.0f)
|
float needLifeTime = 8.0f)
|
||||||
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
|
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
std::string const getName() override { return spell + " on attacker"; }
|
std::string const getName() override { return spell + " on attacker"; }
|
||||||
@ -119,6 +113,19 @@ public:
|
|||||||
CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0);
|
CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0);
|
||||||
|
|
||||||
std::string const GetTargetName() override { return "self target"; }
|
std::string const GetTargetName() override { return "self target"; }
|
||||||
|
bool isUseful() override;
|
||||||
|
bool Execute(Event event) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GroupBuffSpellAction : public CastBuffSpellAction
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GroupBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false,
|
||||||
|
uint32 beforeDuration = 0)
|
||||||
|
: CastBuffSpellAction(botAI, spell, checkIsOwner, beforeDuration) {}
|
||||||
|
|
||||||
|
bool isUseful() override;
|
||||||
|
bool Execute(Event event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastEnchantItemMainHandAction : public CastSpellAction
|
class CastEnchantItemMainHandAction : public CastSpellAction
|
||||||
@ -151,8 +158,6 @@ public:
|
|||||||
// Yunfan: Mana efficiency tell the bot how to save mana. The higher the better.
|
// Yunfan: Mana efficiency tell the bot how to save mana. The higher the better.
|
||||||
HealingManaEfficiency manaEfficiency;
|
HealingManaEfficiency manaEfficiency;
|
||||||
uint8 estAmount;
|
uint8 estAmount;
|
||||||
|
|
||||||
// protected:
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastAoeHealSpellAction : public CastHealingSpellAction
|
class CastAoeHealSpellAction : public CastHealingSpellAction
|
||||||
@ -192,9 +197,7 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA
|
|||||||
public:
|
public:
|
||||||
HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f,
|
HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f,
|
||||||
HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true)
|
HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true)
|
||||||
: CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell)
|
: CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string const GetTargetName() override { return "party member to heal"; }
|
std::string const GetTargetName() override { return "party member to heal"; }
|
||||||
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
||||||
@ -219,9 +222,7 @@ class CurePartyMemberAction : public CastSpellAction, public PartyMemberActionNa
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType)
|
CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType)
|
||||||
: CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType)
|
: CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
||||||
@ -230,7 +231,6 @@ protected:
|
|||||||
uint32 dispelType;
|
uint32 dispelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make Bots Paladin, druid, mage use the greater buff rank spell
|
|
||||||
class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport
|
class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -238,10 +238,18 @@ public:
|
|||||||
: CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
|
: CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
bool Execute(Event event) override;
|
|
||||||
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
||||||
};
|
};
|
||||||
// End Fix
|
|
||||||
|
class GroupBuffOnPartyAction : public GroupBuffSpellAction, public PartyMemberActionNameSupport
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GroupBuffOnPartyAction(PlayerbotAI* botAI, std::string const spell)
|
||||||
|
: GroupBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
|
||||||
|
|
||||||
|
Value<Unit*>* GetTargetValue() override;
|
||||||
|
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
|
||||||
|
};
|
||||||
|
|
||||||
class CastShootAction : public CastSpellAction
|
class CastShootAction : public CastSpellAction
|
||||||
{
|
{
|
||||||
@ -323,6 +331,7 @@ class UseTrinketAction : public Action
|
|||||||
public:
|
public:
|
||||||
UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {}
|
UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {}
|
||||||
bool Execute(Event event) override;
|
bool Execute(Event event) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool UseTrinket(Item* trinket);
|
bool UseTrinket(Item* trinket);
|
||||||
};
|
};
|
||||||
@ -461,12 +470,11 @@ class BuffOnMainTankAction : public CastBuffSpellAction, public MainTankActionNa
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false)
|
BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false)
|
||||||
: CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell)
|
: CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual Value<Unit*>* GetTargetValue();
|
virtual Value<Unit*>* GetTargetValue();
|
||||||
virtual std::string const getName() { return MainTankActionNameSupport::getName(); }
|
virtual std::string const getName() { return MainTankActionNameSupport::getName(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -124,7 +124,6 @@ bool GoAction::Execute(Event event)
|
|||||||
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
|
||||||
{
|
{
|
||||||
PathGenerator path(bot);
|
PathGenerator path(bot);
|
||||||
|
|
||||||
path.CalculatePath(x, y, z, false);
|
path.CalculatePath(x, y, z, false);
|
||||||
|
|
||||||
Movement::Vector3 end = path.GetEndPosition();
|
Movement::Vector3 end = path.GetEndPosition();
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include "LootAction.h"
|
#include "LootAction.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "Bag.h"
|
||||||
#include "ChatHelper.h"
|
#include "ChatHelper.h"
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
#include "GuildMgr.h"
|
#include "GuildMgr.h"
|
||||||
@ -76,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
|
|||||||
bool result = DoLoot(lootObject);
|
bool result = DoLoot(lootObject);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(lootObject.guid);
|
// MarkCompleted (not Remove) — "add all loot" reads
|
||||||
|
// "nearest corpses" without a lootable filter, so a plain
|
||||||
|
// Remove lets the same corpse re-enter the stack on the next
|
||||||
|
// tick. The completed set blocks re-add for ~5 min.
|
||||||
|
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(lootObject.guid);
|
||||||
context->GetValue<LootObject>("loot target")->Set(LootObject());
|
context->GetValue<LootObject>("loot target")->Set(LootObject());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -139,8 +146,9 @@ bool OpenLootAction::DoLoot(LootObject& lootObject)
|
|||||||
if (go && (go->GetGoState() != GO_STATE_READY))
|
if (go && (go->GetGoState() != GO_STATE_READY))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// This prevents dungeon chests like Tribunal Chest (Halls of Stone) from being ninja'd by the bots
|
// Block event-gated chests (Tribunal Chest, Gunship Armory) but allow
|
||||||
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND))
|
// wild quest GOs (Moonpetal Lily etc.) when the bot is on the quest.
|
||||||
|
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !lootObject.isNeededQuestItem)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
|
// This prevents raid chests like Gunship Armory (ICC) from being ninja'd by the bots
|
||||||
@ -377,6 +385,12 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
// bot->GetSession()->HandleLootMoneyOpcode(packet);
|
// bot->GetSession()->HandleLootMoneyOpcode(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// one make-room destroy per loot packet — CanStoreNewItem after a junk
|
||||||
|
// destroy can still report full while CMSG_AUTOSTORE_LOOT_ITEM is
|
||||||
|
// queued, so a multi-quest-item packet would otherwise destroy more
|
||||||
|
// junk than necessary
|
||||||
|
bool destroyedThisPacket = false;
|
||||||
|
|
||||||
for (uint8 i = 0; i < items; ++i)
|
for (uint8 i = 0; i < items; ++i)
|
||||||
{
|
{
|
||||||
uint32 itemid;
|
uint32 itemid;
|
||||||
@ -402,7 +416,9 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
if (!proto)
|
if (!proto)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80)
|
// bags >80%: skip non-stackable junk (quest items exempt)
|
||||||
|
if (!botAI->HasActivePlayerMaster() && AI_VALUE(uint8, "bag space") > 80 &&
|
||||||
|
!bot->HasQuestForItem(itemid))
|
||||||
{
|
{
|
||||||
uint32 maxStack = proto->GetMaxStackSize();
|
uint32 maxStack = proto->GetMaxStackSize();
|
||||||
if (maxStack == 1)
|
if (maxStack == 1)
|
||||||
@ -438,6 +454,55 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
|
GuildTaskMgr::instance().CheckItemTask(itemid, itemcount, ref->GetSource(), bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bags full + quest item: make room by dropping cheapest junk
|
||||||
|
if (!destroyedThisPacket && bot->HasQuestForItem(itemid))
|
||||||
|
{
|
||||||
|
ItemPosCountVec dest;
|
||||||
|
InventoryResult can =
|
||||||
|
bot->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, itemid, itemcount);
|
||||||
|
if (can == EQUIP_ERR_INVENTORY_FULL || can == EQUIP_ERR_BAG_FULL)
|
||||||
|
{
|
||||||
|
// picked by usage, not quality — high-level bots have no grays
|
||||||
|
Item* victim = nullptr;
|
||||||
|
uint32 minPrice = std::numeric_limits<uint32>::max();
|
||||||
|
auto consider = [&](uint8 bag, uint8 slot)
|
||||||
|
{
|
||||||
|
Item* it = bot->GetItemByPos(bag, slot);
|
||||||
|
if (!it)
|
||||||
|
return;
|
||||||
|
ItemTemplate const* tpl = it->GetTemplate();
|
||||||
|
if (!tpl)
|
||||||
|
return;
|
||||||
|
if (bot->HasQuestForItem(tpl->ItemId))
|
||||||
|
return;
|
||||||
|
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", tpl->ItemId);
|
||||||
|
if (usage != ITEM_USAGE_NONE && usage != ITEM_USAGE_VENDOR &&
|
||||||
|
usage != ITEM_USAGE_BAD_EQUIP && usage != ITEM_USAGE_BROKEN_EQUIP)
|
||||||
|
return;
|
||||||
|
if (tpl->SellPrice < minPrice)
|
||||||
|
{
|
||||||
|
minPrice = tpl->SellPrice;
|
||||||
|
victim = it;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
|
||||||
|
consider(INVENTORY_SLOT_BAG_0, slot);
|
||||||
|
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
|
||||||
|
{
|
||||||
|
Bag* pBag = bot->GetBagByPos(bag);
|
||||||
|
if (!pBag)
|
||||||
|
continue;
|
||||||
|
for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot)
|
||||||
|
consider(bag, static_cast<uint8>(slot));
|
||||||
|
}
|
||||||
|
if (victim)
|
||||||
|
{
|
||||||
|
bot->DestroyItem(victim->GetBagSlot(), victim->GetSlot(), true);
|
||||||
|
destroyedThisPacket = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
|
WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1);
|
||||||
*packet << itemindex;
|
*packet << itemindex;
|
||||||
bot->GetSession()->QueuePacket(packet);
|
bot->GetSession()->QueuePacket(packet);
|
||||||
@ -453,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
|
|||||||
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
||||||
}
|
}
|
||||||
|
|
||||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid);
|
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
|
||||||
|
|
||||||
// release loot
|
// release loot
|
||||||
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "Action.h"
|
#include "Action.h"
|
||||||
#include "LastMovementValue.h"
|
#include "LastMovementValue.h"
|
||||||
|
#include "PathGenerator.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
#include "PlayerbotAIConfig.h"
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
@ -22,12 +23,33 @@ class Position;
|
|||||||
#define ANGLE_90_DEG M_PI_2
|
#define ANGLE_90_DEG M_PI_2
|
||||||
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
#define ANGLE_120_DEG (2.f * static_cast<float>(M_PI) / 3.f)
|
||||||
|
|
||||||
|
// Default acceptable path types for GeneratePath
|
||||||
|
constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE;
|
||||||
|
constexpr uint32 RELAXED_PATH_ACCEPT_MASK = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
|
||||||
|
|
||||||
|
struct PathResult
|
||||||
|
{
|
||||||
|
Movement::PointsArray points;
|
||||||
|
G3D::Vector3 actualEnd;
|
||||||
|
G3D::Vector3 end;
|
||||||
|
PathType pathType;
|
||||||
|
bool reachable;
|
||||||
|
};
|
||||||
|
|
||||||
class MovementAction : public Action
|
class MovementAction : public Action
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MovementAction(PlayerbotAI* botAI, std::string const name);
|
MovementAction(PlayerbotAI* botAI, std::string const name);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// Emit a one-line trace describing the imminent movement. No-op
|
||||||
|
// unless the bot has the "debug move" non-combat strategy.
|
||||||
|
// Subclasses (e.g. NewRpgBaseAction) may override to append richer
|
||||||
|
// context such as RPG status and target name. Optional `extra`
|
||||||
|
// is appended verbatim (use it to attach hop labels like
|
||||||
|
// "node:Stormwind innkeeper" or fallback reasons).
|
||||||
|
virtual void EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra = nullptr);
|
||||||
|
|
||||||
bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
|
bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance,
|
||||||
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
@ -50,7 +72,6 @@ protected:
|
|||||||
void SetNextMovementDelay(float delayMillis);
|
void SetNextMovementDelay(float delayMillis);
|
||||||
bool IsMovingAllowed(WorldObject* target);
|
bool IsMovingAllowed(WorldObject* target);
|
||||||
bool IsDuplicateMove(float x, float y, float z);
|
bool IsDuplicateMove(float x, float y, float z);
|
||||||
bool IsWaitingForLastMove(MovementPriority priority);
|
|
||||||
bool IsMovingAllowed();
|
bool IsMovingAllowed();
|
||||||
bool Flee(Unit* target);
|
bool Flee(Unit* target);
|
||||||
void ClearIdleState();
|
void ClearIdleState();
|
||||||
@ -66,6 +87,30 @@ protected:
|
|||||||
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
bool FleePosition(Position pos, float radius, uint32 minInterval = 1000);
|
||||||
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
bool CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList);
|
||||||
|
|
||||||
|
PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = false);
|
||||||
|
|
||||||
|
bool GetTravelPlan(TravelPlan& plan, WorldPosition destination);
|
||||||
|
bool ExecuteTravelPlan(TravelPlan& state);
|
||||||
|
|
||||||
|
// Transport boarding helpers (shared by FollowAction and travel plan)
|
||||||
|
static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref,
|
||||||
|
uint32 phaseMask, float x, float y, float z);
|
||||||
|
static bool FindBoardingPointOnTransport(Map* map, Transport* transport,
|
||||||
|
WorldObject* ref, float refX, float refY, float refZ,
|
||||||
|
float botX, float botY, float botZ,
|
||||||
|
float& outX, float& outY, float& outZ);
|
||||||
|
bool BoardTransport(Transport* transport);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool LaunchWalkSpline(TravelPlan& state);
|
||||||
|
bool MoveToSpline(TravelPlan& state, WorldPosition target);
|
||||||
|
// Per-segment mmap refinement of a travel-node-graph walk batch.
|
||||||
|
// The graph stores offline-baked coords whose straight-line
|
||||||
|
// interpolation may pass through geometry the bot can't actually
|
||||||
|
// traverse. Returns false if any segment is unwalkable per the
|
||||||
|
// live navmesh, in which case the caller should abort the plan.
|
||||||
|
bool RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct CheckAngle
|
struct CheckAngle
|
||||||
{
|
{
|
||||||
@ -74,10 +119,6 @@ protected:
|
|||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// float SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range = 20.0f, bool
|
|
||||||
// normal_only = false, float step = 8.0f);
|
|
||||||
const Movement::PointsArray SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount = 5,
|
|
||||||
bool normal_only = false, float step = 8.0f);
|
|
||||||
bool wasMovementRestricted = false;
|
bool wasMovementRestricted = false;
|
||||||
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
|
void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "GenericBuffUtils.h"
|
||||||
#include "CreatureAI.h"
|
#include "CreatureAI.h"
|
||||||
#include "ItemVisitors.h"
|
#include "ItemVisitors.h"
|
||||||
#include "LastSpellCastValue.h"
|
#include "LastSpellCastValue.h"
|
||||||
@ -41,52 +42,50 @@ bool LowEnergyTrigger::IsActive()
|
|||||||
|
|
||||||
bool NoPetTrigger::IsActive()
|
bool NoPetTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) &&
|
return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() &&
|
||||||
(!bot->GetFirstControlled()) && (!AI_VALUE2(bool, "mounted", "self target"));
|
!bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasPetTrigger::IsActive()
|
bool HasPetTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return (AI_VALUE(Unit*, "pet target")) && !AI_VALUE2(bool, "mounted", "self target");
|
return AI_VALUE(Unit*, "pet target") && !AI_VALUE2(bool, "mounted", "self target");
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PetAttackTrigger::IsActive()
|
bool PetAttackTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Guardian* pet = bot->GetGuardianPet();
|
Guardian* pet = bot->GetGuardianPet();
|
||||||
if (!pet)
|
if (!pet)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
if (!target)
|
if (!target)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack())
|
if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
|
if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HighManaTrigger::IsActive()
|
bool HighManaTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana;
|
return AI_VALUE2(bool, "has mana", "self target") &&
|
||||||
|
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AlmostFullManaTrigger::IsActive()
|
bool AlmostFullManaTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > 85;
|
return AI_VALUE2(bool, "has mana", "self target") &&
|
||||||
|
AI_VALUE2(uint8, "mana", "self target") > 85;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EnoughManaTrigger::IsActive()
|
bool EnoughManaTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana;
|
return AI_VALUE2(bool, "has mana", "self target") &&
|
||||||
|
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; }
|
bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; }
|
||||||
@ -101,9 +100,8 @@ bool TargetWithComboPointsLowerHealTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
if (!target || !target->IsAlive() || !target->IsInWorld())
|
if (!target || !target->IsAlive() || !target->IsInWorld())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return ComboPointsAvailableTrigger::IsActive() &&
|
return ComboPointsAvailableTrigger::IsActive() &&
|
||||||
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime;
|
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime;
|
||||||
}
|
}
|
||||||
@ -164,19 +162,27 @@ bool BuffTrigger::IsActive()
|
|||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
if (!SpellTrigger::IsActive())
|
|
||||||
return false;
|
|
||||||
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
|
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
|
||||||
if (!aura)
|
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
|
||||||
return true;
|
|
||||||
if (beforeDuration && aura->GetDuration() < beforeDuration)
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Value<Unit*>* BuffOnPartyTrigger::GetTargetValue()
|
Value<Unit*>* BuffOnPartyTrigger::GetTargetValue()
|
||||||
{
|
{
|
||||||
return context->GetValue<Unit*>("party member without aura", spell);
|
return context->GetValue<Unit*>(
|
||||||
|
"party member without aura", ai::buff::MakeAuraQualifierForBuff(spell));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuffOnPartyTrigger::IsActive()
|
||||||
|
{
|
||||||
|
Unit* target = GetTarget();
|
||||||
|
if (ai::buff::ShouldDeferPartyBuffEvaluationForRecentLogin(bot, target, spell))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return BuffTrigger::IsActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); }
|
bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); }
|
||||||
@ -209,13 +215,14 @@ bool MediumThreatTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
if (!AI_VALUE(Unit*, "main tank"))
|
if (!AI_VALUE(Unit*, "main tank"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return MyAttackerCountTrigger::IsActive();
|
return MyAttackerCountTrigger::IsActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LowTankThreatTrigger::IsActive()
|
bool LowTankThreatTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Unit* mt = AI_VALUE(Unit*, "main tank");
|
Unit* mainTank = AI_VALUE(Unit*, "main tank");
|
||||||
if (!mt)
|
if (!mainTank)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Unit* current_target = AI_VALUE(Unit*, "current target");
|
Unit* current_target = AI_VALUE(Unit*, "current target");
|
||||||
@ -224,7 +231,7 @@ bool LowTankThreatTrigger::IsActive()
|
|||||||
|
|
||||||
ThreatManager& mgr = current_target->GetThreatMgr();
|
ThreatManager& mgr = current_target->GetThreatMgr();
|
||||||
float threat = mgr.GetThreat(bot);
|
float threat = mgr.GetThreat(bot);
|
||||||
float tankThreat = mgr.GetThreat(mt);
|
float tankThreat = mgr.GetThreat(mainTank);
|
||||||
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
|
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +239,8 @@ bool AoeTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
Unit* current_target = AI_VALUE(Unit*, "current target");
|
Unit* current_target = AI_VALUE(Unit*, "current target");
|
||||||
if (!current_target)
|
if (!current_target)
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get();
|
GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get();
|
||||||
int attackers_count = 0;
|
int attackers_count = 0;
|
||||||
for (ObjectGuid const guid : attackers)
|
for (ObjectGuid const guid : attackers)
|
||||||
@ -242,11 +248,10 @@ bool AoeTrigger::IsActive()
|
|||||||
Unit* unit = botAI->GetUnit(guid);
|
Unit* unit = botAI->GetUnit(guid);
|
||||||
if (!unit || !unit->IsAlive())
|
if (!unit || !unit->IsAlive())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (unit->GetDistance(current_target->GetPosition()) <= range)
|
if (unit->GetDistance(current_target->GetPosition()) <= range)
|
||||||
{
|
|
||||||
attackers_count++;
|
attackers_count++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return attackers_count >= amount;
|
return attackers_count >= amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,20 +279,19 @@ bool DebuffTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target || !target->IsAlive() || !target->IsInWorld())
|
if (!target || !target->IsAlive() || !target->IsInWorld())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return BuffTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
|
return BuffTrigger::IsActive() &&
|
||||||
|
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DebuffOnBossTrigger::IsActive()
|
bool DebuffOnBossTrigger::IsActive()
|
||||||
{
|
{
|
||||||
if (!DebuffTrigger::IsActive())
|
if (!DebuffTrigger::IsActive())
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
Creature* c = GetTarget()->ToCreature();
|
Creature* creature = GetTarget()->ToCreature();
|
||||||
return c && ((c->IsDungeonBoss()) || (c->isWorldBoss()));
|
return creature && (creature->IsDungeonBoss() || creature->isWorldBoss());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SpellTrigger::IsActive() { return GetTarget(); }
|
bool SpellTrigger::IsActive() { return GetTarget(); }
|
||||||
@ -317,9 +321,7 @@ bool SpellCooldownTrigger::IsActive()
|
|||||||
}
|
}
|
||||||
|
|
||||||
RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability)
|
RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability)
|
||||||
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime())
|
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RandomTrigger::IsActive()
|
bool RandomTrigger::IsActive()
|
||||||
{
|
{
|
||||||
@ -330,6 +332,7 @@ bool RandomTrigger::IsActive()
|
|||||||
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
|
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
|
||||||
if (k < 1)
|
if (k < 1)
|
||||||
k = 1;
|
k = 1;
|
||||||
|
|
||||||
return (rand() % k) == 0;
|
return (rand() % k) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,9 +371,11 @@ bool BoostTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
if (!BuffTrigger::IsActive())
|
if (!BuffTrigger::IsActive())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
if (target && target->ToPlayer())
|
if (target && target->ToPlayer())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return AI_VALUE(uint8, "balance") <= balance;
|
return AI_VALUE(uint8, "balance") <= balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,20 +384,19 @@ bool GenericBoostTrigger::IsActive()
|
|||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
if (target && target->ToPlayer())
|
if (target && target->ToPlayer())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return AI_VALUE(uint8, "balance") <= balance;
|
return AI_VALUE(uint8, "balance") <= balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HealerShouldAttackTrigger::IsActive()
|
bool HealerShouldAttackTrigger::IsActive()
|
||||||
{
|
{
|
||||||
// nobody can help me
|
|
||||||
if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1)
|
if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth)
|
if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// special check for resto druid (dont remove tree of life frequently)
|
if (bot->GetAura(33891)) // Tree of Life
|
||||||
if (bot->GetAura(33891))
|
|
||||||
{
|
{
|
||||||
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
|
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
|
||||||
if (lastSpell.timer + 5 > time(nullptr))
|
if (lastSpell.timer + 5 > time(nullptr))
|
||||||
@ -401,7 +405,6 @@ bool HealerShouldAttackTrigger::IsActive()
|
|||||||
|
|
||||||
int manaThreshold;
|
int manaThreshold;
|
||||||
int balance = AI_VALUE(uint8, "balance");
|
int balance = AI_VALUE(uint8, "balance");
|
||||||
// higher threshold in higher pressure
|
|
||||||
if (balance <= 50)
|
if (balance <= 50)
|
||||||
manaThreshold = 85;
|
manaThreshold = 85;
|
||||||
else if (balance <= 100)
|
else if (balance <= 100)
|
||||||
@ -425,13 +428,7 @@ bool InterruptSpellTrigger::IsActive()
|
|||||||
bool DeflectSpellTrigger::IsActive()
|
bool DeflectSpellTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID())
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!target->IsNonMeleeSpellCast(true))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target->GetTarget() != bot->GetGUID())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
|
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
|
||||||
@ -462,6 +459,7 @@ bool DeflectSpellTrigger::IsActive()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,17 +493,16 @@ bool FearSleepSapTrigger::IsActive()
|
|||||||
|
|
||||||
bool HasAuraStackTrigger::IsActive()
|
bool HasAuraStackTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);
|
return botAI->GetAura(getName(), GetTarget(), false, true, stack);
|
||||||
// sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ?
|
|
||||||
// aura->GetStackAmount() : -1);
|
|
||||||
return aura;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TimerTrigger::IsActive()
|
bool TimerTrigger::IsActive()
|
||||||
{
|
{
|
||||||
if (time(nullptr) != lastCheck)
|
time_t now = time(nullptr);
|
||||||
|
|
||||||
|
if (now != lastCheck)
|
||||||
{
|
{
|
||||||
lastCheck = time(nullptr);
|
lastCheck = now;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,9 +549,8 @@ bool IsBehindTargetTrigger::IsActive()
|
|||||||
bool IsNotBehindTargetTrigger::IsActive()
|
bool IsNotBehindTargetTrigger::IsActive()
|
||||||
{
|
{
|
||||||
if (botAI->HasStrategy("stay", botAI->GetState()))
|
if (botAI->HasStrategy("stay", botAI->GetState()))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
return target && !AI_VALUE2(bool, "behind", "current target");
|
return target && !AI_VALUE2(bool, "behind", "current target");
|
||||||
}
|
}
|
||||||
@ -562,9 +558,8 @@ bool IsNotBehindTargetTrigger::IsActive()
|
|||||||
bool IsNotFacingTargetTrigger::IsActive()
|
bool IsNotFacingTargetTrigger::IsActive()
|
||||||
{
|
{
|
||||||
if (botAI->HasStrategy("stay", botAI->GetState()))
|
if (botAI->HasStrategy("stay", botAI->GetState()))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
return !AI_VALUE2(bool, "facing", "current target");
|
return !AI_VALUE2(bool, "facing", "current target");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,12 +576,14 @@ bool NoPossibleTargetsTrigger::IsActive()
|
|||||||
return !targets.size();
|
return !targets.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PossibleAddsTrigger::IsActive() { return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); }
|
bool PossibleAddsTrigger::IsActive()
|
||||||
|
{
|
||||||
|
return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target");
|
||||||
|
}
|
||||||
|
|
||||||
bool NotDpsTargetActiveTrigger::IsActive()
|
bool NotDpsTargetActiveTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
// do not switch if enemy target
|
|
||||||
if (target && target->IsAlive())
|
if (target && target->IsAlive())
|
||||||
{
|
{
|
||||||
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
|
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
|
||||||
@ -604,7 +601,6 @@ bool NotDpsAoeTargetActiveTrigger::IsActive()
|
|||||||
Unit* target = AI_VALUE(Unit*, "current target");
|
Unit* target = AI_VALUE(Unit*, "current target");
|
||||||
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
|
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
|
||||||
|
|
||||||
// do not switch if enemy target
|
|
||||||
if (target && target == enemy && target->IsAlive())
|
if (target && target == enemy && target->IsAlive())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -638,7 +634,10 @@ Value<Unit*>* InterruptEnemyHealerTrigger::GetTargetValue()
|
|||||||
return context->GetValue<Unit*>("enemy healer target", spell);
|
return context->GetValue<Unit*>("enemy healer target", spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RandomBotUpdateTrigger::IsActive() { return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); }
|
bool RandomBotUpdateTrigger::IsActive()
|
||||||
|
{
|
||||||
|
return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update");
|
||||||
|
}
|
||||||
|
|
||||||
bool NoNonBotPlayersAroundTrigger::IsActive()
|
bool NoNonBotPlayersAroundTrigger::IsActive()
|
||||||
{
|
{
|
||||||
@ -718,43 +717,24 @@ bool AmmoCountTrigger::IsActive()
|
|||||||
|
|
||||||
bool NewPetTrigger::IsActive()
|
bool NewPetTrigger::IsActive()
|
||||||
{
|
{
|
||||||
// Get the bot player object from the AI
|
|
||||||
Player* bot = botAI->GetBot();
|
|
||||||
if (!bot)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Try to get the current pet; initialize guardian and GUID to null/empty
|
|
||||||
Pet* pet = bot->GetPet();
|
|
||||||
Guardian* guardian = nullptr;
|
|
||||||
ObjectGuid currentPetGuid = ObjectGuid::Empty;
|
ObjectGuid currentPetGuid = ObjectGuid::Empty;
|
||||||
|
|
||||||
// If bot has a pet, get its GUID
|
if (Pet* pet = bot->GetPet())
|
||||||
if (pet)
|
|
||||||
{
|
|
||||||
currentPetGuid = pet->GetGUID();
|
currentPetGuid = pet->GetGUID();
|
||||||
}
|
else if (Guardian* guardian = bot->GetGuardianPet())
|
||||||
else
|
|
||||||
{
|
|
||||||
// If no pet, try to get a guardian pet and its GUID
|
|
||||||
guardian = bot->GetGuardianPet();
|
|
||||||
if (guardian)
|
|
||||||
currentPetGuid = guardian->GetGUID();
|
currentPetGuid = guardian->GetGUID();
|
||||||
}
|
|
||||||
|
|
||||||
// If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state
|
|
||||||
if (currentPetGuid != lastPetGuid)
|
if (currentPetGuid != lastPetGuid)
|
||||||
{
|
{
|
||||||
triggered = false;
|
triggered = false;
|
||||||
lastPetGuid = currentPetGuid;
|
lastPetGuid = currentPetGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a valid current pet/guardian (non-empty GUID) and we haven't triggered yet, activate trigger
|
|
||||||
if (currentPetGuid != ObjectGuid::Empty && !triggered)
|
if (currentPetGuid != ObjectGuid::Empty && !triggered)
|
||||||
{
|
{
|
||||||
triggered = true;
|
triggered = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, do not activate
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,7 @@ class StatAvailable : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available")
|
StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available")
|
||||||
: Trigger(botAI, name), amount(amount)
|
: Trigger(botAI, name), amount(amount) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int32 amount;
|
int32 amount;
|
||||||
@ -118,8 +116,8 @@ public:
|
|||||||
class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger
|
class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f)
|
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f)
|
||||||
: ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime)
|
: ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
@ -196,7 +194,6 @@ public:
|
|||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: check other targets
|
|
||||||
class InterruptSpellTrigger : public SpellTrigger
|
class InterruptSpellTrigger : public SpellTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -217,9 +214,7 @@ class AttackerCountTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance)
|
AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance)
|
||||||
: Trigger(botAI), amount(amount), distance(distance)
|
: Trigger(botAI), amount(amount), distance(distance) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
std::string const getName() override { return "attacker count"; }
|
std::string const getName() override { return "attacker count"; }
|
||||||
@ -269,9 +264,7 @@ class AoeTrigger : public AttackerCountTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f)
|
AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f)
|
||||||
: AttackerCountTrigger(botAI, amount), range(range)
|
: AttackerCountTrigger(botAI, amount), range(range) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
std::string const getName() override { return "aoe"; }
|
std::string const getName() override { return "aoe"; }
|
||||||
@ -317,7 +310,8 @@ public:
|
|||||||
class BuffTrigger : public SpellTrigger
|
class BuffTrigger : public SpellTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0)
|
BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1,
|
||||||
|
bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0)
|
||||||
: SpellTrigger(botAI, spell, checkInterval)
|
: SpellTrigger(botAI, spell, checkInterval)
|
||||||
{
|
{
|
||||||
this->checkIsOwner = checkIsOwner;
|
this->checkIsOwner = checkIsOwner;
|
||||||
@ -339,11 +333,10 @@ class BuffOnPartyTrigger : public BuffTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
|
BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
|
||||||
: BuffTrigger(botAI, spell, checkInterval)
|
: BuffTrigger(botAI, spell, checkInterval) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
|
bool IsActive() override;
|
||||||
std::string const getName() override { return spell + " on party"; }
|
std::string const getName() override { return spell + " on party"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -393,9 +386,7 @@ class DebuffTrigger : public BuffTrigger
|
|||||||
public:
|
public:
|
||||||
DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false,
|
DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false,
|
||||||
float needLifeTime = 8.0f, uint32 beforeDuration = 0)
|
float needLifeTime = 8.0f, uint32 beforeDuration = 0)
|
||||||
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime)
|
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string const GetTargetName() override { return "current target"; }
|
std::string const GetTargetName() override { return "current target"; }
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
@ -408,9 +399,7 @@ class DebuffOnBossTrigger : public DebuffTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false)
|
DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false)
|
||||||
: DebuffTrigger(botAI, spell, checkInterval, checkIsOwner)
|
: DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -419,9 +408,7 @@ class DebuffOnAttackerTrigger : public DebuffTrigger
|
|||||||
public:
|
public:
|
||||||
DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
|
DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
|
||||||
float needLifeTime = 8.0f)
|
float needLifeTime = 8.0f)
|
||||||
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
|
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
std::string const getName() override { return spell + " on attacker"; }
|
std::string const getName() override { return spell + " on attacker"; }
|
||||||
@ -432,9 +419,7 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger
|
|||||||
public:
|
public:
|
||||||
DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
|
DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
|
||||||
float needLifeTime = 8.0f)
|
float needLifeTime = 8.0f)
|
||||||
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
|
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
std::string const getName() override { return spell + " on attacker"; }
|
std::string const getName() override { return spell + " on attacker"; }
|
||||||
@ -444,9 +429,7 @@ class BoostTrigger : public BuffTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f)
|
BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f)
|
||||||
: BuffTrigger(botAI, spell, 1), balance(balance)
|
: BuffTrigger(botAI, spell, 1), balance(balance) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
@ -458,9 +441,7 @@ class GenericBoostTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f)
|
GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f)
|
||||||
: Trigger(botAI, "generic boost", 1), balance(balance)
|
: Trigger(botAI, "generic boost", 1), balance(balance) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
@ -472,9 +453,7 @@ class HealerShouldAttackTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
HealerShouldAttackTrigger(PlayerbotAI* botAI)
|
HealerShouldAttackTrigger(PlayerbotAI* botAI)
|
||||||
: Trigger(botAI, "healer should attack", 1)
|
: Trigger(botAI, "healer should attack", 1) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -580,7 +559,7 @@ public:
|
|||||||
class HasPetTrigger : public Trigger
|
class HasPetTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {}
|
HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {}
|
||||||
|
|
||||||
virtual bool IsActive() override;
|
virtual bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -588,7 +567,7 @@ public:
|
|||||||
class PetAttackTrigger : public Trigger
|
class PetAttackTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {}
|
PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {}
|
||||||
|
|
||||||
virtual bool IsActive() override;
|
virtual bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -597,9 +576,7 @@ class ItemCountTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000)
|
ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000)
|
||||||
: Trigger(botAI, item, interval), item(item), count(count)
|
: Trigger(botAI, item, interval), item(item), count(count) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
std::string const getName() override { return "item count"; }
|
std::string const getName() override { return "item count"; }
|
||||||
@ -613,9 +590,7 @@ class AmmoCountTrigger : public ItemCountTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000)
|
AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000)
|
||||||
: ItemCountTrigger(botAI, item, count, interval)
|
: ItemCountTrigger(botAI, item, count, interval) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -623,9 +598,7 @@ class HasAuraTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
|
HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
|
||||||
: Trigger(botAI, spell, checkInterval)
|
: Trigger(botAI, spell, checkInterval) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string const GetTargetName() override { return "self target"; }
|
std::string const GetTargetName() override { return "self target"; }
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
@ -634,10 +607,8 @@ public:
|
|||||||
class HasAuraStackTrigger : public Trigger
|
class HasAuraStackTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1)
|
HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1)
|
||||||
: Trigger(ai, spell, checkInterval), stack(stack)
|
: Trigger(botAI, spell, checkInterval), stack(stack) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string const GetTargetName() override { return "self target"; }
|
std::string const GetTargetName() override { return "self target"; }
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
@ -858,9 +829,7 @@ class StayTimeTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name)
|
StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name)
|
||||||
: Trigger(botAI, name, 5 * 1000), delay(delay)
|
: Trigger(botAI, name, 5 * 1000), delay(delay) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
@ -877,7 +846,7 @@ public:
|
|||||||
class ReturnToStayPositionTrigger : public Trigger
|
class ReturnToStayPositionTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {}
|
ReturnToStayPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to stay position", 2) {}
|
||||||
|
|
||||||
virtual bool IsActive() override;
|
virtual bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -892,9 +861,7 @@ class GiveItemTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item)
|
GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item)
|
||||||
: Trigger(botAI, name, 2 * 1000), item(item)
|
: Trigger(botAI, name, 2 * 1000), item(item) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
@ -962,9 +929,7 @@ class BuffOnMainTankTrigger : public BuffTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1)
|
BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1)
|
||||||
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner)
|
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual Value<Unit*>* GetTargetValue();
|
virtual Value<Unit*>* GetTargetValue();
|
||||||
@ -973,7 +938,7 @@ public:
|
|||||||
class SelfResurrectTrigger : public Trigger
|
class SelfResurrectTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SelfResurrectTrigger(PlayerbotAI* ai) : Trigger(ai, "can self resurrect") {}
|
SelfResurrectTrigger(PlayerbotAI* botAI) : Trigger(botAI, "can self resurrect") {}
|
||||||
|
|
||||||
bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); }
|
bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); }
|
||||||
};
|
};
|
||||||
@ -981,7 +946,7 @@ public:
|
|||||||
class NewPetTrigger : public Trigger
|
class NewPetTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
|
NewPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
|
|||||||
@ -11,21 +11,20 @@
|
|||||||
|
|
||||||
bool LootAvailableTrigger::IsActive()
|
bool LootAvailableTrigger::IsActive()
|
||||||
{
|
{
|
||||||
bool distanceCheck = false;
|
// Strategy is non-combat-only — the engine state separation is the
|
||||||
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
|
// safety net. Don't gate on hostiles-in-sight: that locked out
|
||||||
{
|
// looting in zones with continuous respawns (e.g. cave farms).
|
||||||
distanceCheck =
|
// If a new enemy aggros mid-loot the combat engine takes over, loot
|
||||||
ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
|
// resumes on the next non-combat window.
|
||||||
}
|
if (!AI_VALUE(bool, "has available loot"))
|
||||||
else
|
return false;
|
||||||
{
|
|
||||||
distanceCheck = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"),
|
|
||||||
INTERACTION_DISTANCE - 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if loot target if empty, always pass distance check
|
// "stay" strategy is restrictive: only loot if corpse is at our feet.
|
||||||
return AI_VALUE(bool, "has available loot") &&
|
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
|
||||||
(distanceCheck || AI_VALUE(GuidVector, "all targets").empty());
|
return ServerFacade::instance().IsDistanceLessOrEqualThan(
|
||||||
|
AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FarFromCurrentLootTrigger::IsActive()
|
bool FarFromCurrentLootTrigger::IsActive()
|
||||||
|
|||||||
@ -4,23 +4,89 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "GenericBuffUtils.h"
|
#include "GenericBuffUtils.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include "Player.h"
|
|
||||||
#include "Group.h"
|
|
||||||
#include "SpellMgr.h"
|
|
||||||
#include "Chat.h"
|
|
||||||
#include "PlayerbotAI.h"
|
|
||||||
#include "ServerFacade.h"
|
|
||||||
#include "AiObjectContext.h"
|
#include "AiObjectContext.h"
|
||||||
|
|
||||||
|
#include "GameTime.h"
|
||||||
|
#include "Group.h"
|
||||||
|
#include "Player.h"
|
||||||
|
#include "PlayerbotAI.h"
|
||||||
|
#include "PlayerbotAIConfig.h"
|
||||||
|
#include "SpellMgr.h"
|
||||||
|
#include "Unit.h"
|
||||||
#include "Value.h"
|
#include "Value.h"
|
||||||
#include "Config.h"
|
|
||||||
#include "PlayerbotTextMgr.h"
|
|
||||||
|
|
||||||
namespace ai::buff
|
namespace ai::buff
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Prevents bots from immediately casting already-present buffs upon logging in
|
||||||
|
constexpr uint32 POST_LOGIN_BUFF_GRACE_MS = 5 * IN_MILLISECONDS;
|
||||||
|
|
||||||
|
bool IsWithinPostLoginBuffGrace(Player* player)
|
||||||
|
{
|
||||||
|
if (!player)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return getMSTimeDiff(
|
||||||
|
player->GetInGameTime(), GameTime::GetGameTimeMS().count()) < POST_LOGIN_BUFF_GRACE_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasEnoughSameMapMissingPlayersForGroupVariant(
|
||||||
|
Player* bot, PlayerbotAI* botAI, std::string const& baseName,
|
||||||
|
std::string const& groupName, uint32 requiredCount = 3)
|
||||||
|
{
|
||||||
|
Group* group = bot->GetGroup();
|
||||||
|
if (!group)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32 missingCount = 0;
|
||||||
|
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
|
||||||
|
{
|
||||||
|
Player* member = gref->GetSource();
|
||||||
|
if (!member || !member->IsInWorld() || !member->IsAlive() ||
|
||||||
|
member->GetMap() != bot->GetMap())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (botAI->HasAura(baseName, member) || botAI->HasAura(groupName, member))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (++missingCount >= requiredCount)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsEligibleGroupForPartyBuffs(Group const* group)
|
||||||
|
{
|
||||||
|
if (!group)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (sPlayerbotAIConfig.autoPartyBuffs)
|
||||||
|
{
|
||||||
|
case AutoPartyBuffMode::RAID_ONLY:
|
||||||
|
return group->isRaidGroup();
|
||||||
|
case AutoPartyBuffMode::GROUP_OR_RAID:
|
||||||
|
return true;
|
||||||
|
case AutoPartyBuffMode::DISABLED:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsGroupVariantEnabled(Player* bot, std::string const& name)
|
||||||
|
{
|
||||||
|
if (!IsEligibleGroupForPartyBuffs(bot->GetGroup()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !GroupVariantFor(name).empty();
|
||||||
|
}
|
||||||
|
|
||||||
std::string MakeAuraQualifierForBuff(std::string const& name)
|
std::string MakeAuraQualifierForBuff(std::string const& name)
|
||||||
{
|
{
|
||||||
// Paladin
|
// Paladin
|
||||||
@ -34,27 +100,89 @@ namespace ai::buff
|
|||||||
if (name == "arcane intellect") return "arcane intellect,arcane brilliance";
|
if (name == "arcane intellect") return "arcane intellect,arcane brilliance";
|
||||||
// Priest
|
// Priest
|
||||||
if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude";
|
if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude";
|
||||||
|
if (name == "divine spirit") return "divine spirit,prayer of spirit";
|
||||||
|
if (name == "shadow protection") return "shadow protection,prayer of shadow protection";
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GroupVariantFor(std::string const& name)
|
std::string GroupVariantFor(std::string const& name)
|
||||||
{
|
{
|
||||||
// Paladin
|
|
||||||
if (name == "blessing of kings") return "greater blessing of kings";
|
|
||||||
if (name == "blessing of might") return "greater blessing of might";
|
|
||||||
if (name == "blessing of wisdom") return "greater blessing of wisdom";
|
|
||||||
if (name == "blessing of sanctuary") return "greater blessing of sanctuary";
|
|
||||||
// Druid
|
// Druid
|
||||||
if (name == "mark of the wild") return "gift of the wild";
|
if (name == "mark of the wild") return "gift of the wild";
|
||||||
// Mage
|
// Mage
|
||||||
if (name == "arcane intellect") return "arcane brilliance";
|
if (name == "arcane intellect") return "arcane brilliance";
|
||||||
// Priest
|
// Priest
|
||||||
if (name == "power word: fortitude") return "prayer of fortitude";
|
if (name == "power word: fortitude") return "prayer of fortitude";
|
||||||
|
if (name == "divine spirit") return "prayer of spirit";
|
||||||
|
if (name == "shadow protection") return "prayer of shadow protection";
|
||||||
|
|
||||||
|
// Paladin blessings are intentionally not included here because they are
|
||||||
|
// coordinated by the auto greater blessing system instead.
|
||||||
return std::string();
|
return std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NeedsPostLoginBuffGrace(std::string const& name)
|
||||||
|
{
|
||||||
|
static char const* const trackedBuffs[] = {
|
||||||
|
"mark of the wild",
|
||||||
|
"arcane intellect",
|
||||||
|
"power word: fortitude",
|
||||||
|
"prayer of fortitude",
|
||||||
|
"divine spirit",
|
||||||
|
"prayer of spirit",
|
||||||
|
"shadow protection",
|
||||||
|
"prayer of shadow protection",
|
||||||
|
"blessing of kings",
|
||||||
|
"blessing of might",
|
||||||
|
"blessing of wisdom",
|
||||||
|
"blessing of sanctuary"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (char const* trackedBuff : trackedBuffs)
|
||||||
|
{
|
||||||
|
if (name.find(trackedBuff) != std::string::npos)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
|
||||||
|
Player* bot, Unit* target, std::string const& spell)
|
||||||
|
{
|
||||||
|
if (!NeedsPostLoginBuffGrace(spell))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsWithinPostLoginBuffGrace(bot))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Player* playerTarget = target ? target->ToPlayer() : nullptr;
|
||||||
|
return IsWithinPostLoginBuffGrace(playerTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot)
|
||||||
|
{
|
||||||
|
if (IsWithinPostLoginBuffGrace(bot))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Group* group = bot->GetGroup();
|
||||||
|
if (!group)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
|
||||||
|
{
|
||||||
|
Player* member = gref->GetSource();
|
||||||
|
if (!member || !member->IsInWorld())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (IsWithinPostLoginBuffGrace(member))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool HasRequiredReagents(Player* bot, uint32 spellId)
|
bool HasRequiredReagents(Player* bot, uint32 spellId)
|
||||||
{
|
{
|
||||||
if (!spellId)
|
if (!spellId)
|
||||||
@ -72,75 +200,33 @@ namespace ai::buff
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No reagent required
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string UpgradeToGroupIfAppropriate(
|
std::string UpgradeToGroupIfAppropriate(
|
||||||
Player* bot,
|
Player* bot, PlayerbotAI* botAI, std::string const& baseName)
|
||||||
PlayerbotAI* botAI,
|
|
||||||
std::string const& baseName,
|
|
||||||
bool announceOnMissing,
|
|
||||||
std::function<void(std::string const&)> announce)
|
|
||||||
{
|
{
|
||||||
std::string castName = baseName;
|
if (!IsGroupVariantEnabled(bot, baseName))
|
||||||
Group* g = bot->GetGroup();
|
return baseName;
|
||||||
if (!g || g->GetMembersCount() < static_cast<uint32>(sPlayerbotAIConfig.minBotsForGreaterBuff))
|
|
||||||
return castName; // Group too small: stay in solo mode
|
|
||||||
|
|
||||||
if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty())
|
std::string const groupName = GroupVariantFor(baseName);
|
||||||
{
|
if (groupName.empty())
|
||||||
uint32 const groupVariantSpellId = botAI->GetAiObjectContext()
|
return baseName;
|
||||||
|
|
||||||
|
// Prefer singles until at least three living, in-world group members on the bot's map
|
||||||
|
// are missing both the single-target buff and its group variant.
|
||||||
|
if (!HasEnoughSameMapMissingPlayersForGroupVariant(bot, botAI, baseName, groupName))
|
||||||
|
return baseName;
|
||||||
|
|
||||||
|
uint32 const groupSpellId = botAI->GetAiObjectContext()
|
||||||
->GetValue<uint32>("spell id", groupName)->Get();
|
->GetValue<uint32>("spell id", groupName)->Get();
|
||||||
|
|
||||||
// We check usefulness on the **basic** buff (not the greater version),
|
if (groupSpellId && HasRequiredReagents(bot, groupSpellId))
|
||||||
// because "spell cast useful" may return false for the greater variant.
|
|
||||||
bool const usefulBase = botAI->GetAiObjectContext()
|
|
||||||
->GetValue<bool>("spell cast useful", baseName)->Get();
|
|
||||||
|
|
||||||
if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId))
|
|
||||||
{
|
|
||||||
// Learned + reagents OK -> switch to greater
|
|
||||||
return groupName;
|
return groupName;
|
||||||
}
|
|
||||||
|
|
||||||
// Missing reagents -> announce if (a) greater is known, (b) base buff is useful,
|
return baseName;
|
||||||
// (c) announce was requested, (d) a callback is provided.
|
|
||||||
if (announceOnMissing && groupVariantSpellId && usefulBase && announce)
|
|
||||||
{
|
|
||||||
static std::map<std::pair<uint32, std::string>, time_t> s_lastWarn; // par bot & par buff
|
|
||||||
time_t now = std::time(nullptr);
|
|
||||||
uint32 botLow = static_cast<uint32>(bot->GetGUID().GetCounter());
|
|
||||||
time_t& last = s_lastWarn[ std::make_pair(botLow, groupName) ];
|
|
||||||
if (!last || now - last >= sPlayerbotAIConfig.rpWarningCooldown) // Configurable anti-spam
|
|
||||||
{
|
|
||||||
// DB Key choice in regard of the buff
|
|
||||||
std::string key;
|
|
||||||
if (groupName.find("greater blessing") != std::string::npos)
|
|
||||||
key = "rp_missing_reagent_greater_blessing";
|
|
||||||
else if (groupName == "gift of the wild")
|
|
||||||
key = "rp_missing_reagent_gift_of_the_wild";
|
|
||||||
else if (groupName == "arcane brilliance")
|
|
||||||
key = "rp_missing_reagent_arcane_brilliance";
|
|
||||||
else
|
|
||||||
key = "rp_missing_reagent_generic";
|
|
||||||
|
|
||||||
// Placeholders
|
|
||||||
std::map<std::string, std::string> placeholders;
|
|
||||||
placeholders["%group_spell"] = groupName;
|
|
||||||
placeholders["%base_spell"] = baseName;
|
|
||||||
|
|
||||||
std::string announceText = PlayerbotTextMgr::instance().GetBotTextOrDefault(key,
|
|
||||||
"Out of components for %group_spell. Using %base_spell!", placeholders);
|
|
||||||
|
|
||||||
announce(announceText);
|
|
||||||
last = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return castName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,63 +6,40 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "Group.h"
|
|
||||||
#include "Chat.h"
|
|
||||||
#include "Language.h"
|
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
class PlayerbotAI;
|
class PlayerbotAI;
|
||||||
|
class Unit;
|
||||||
|
|
||||||
namespace ai::buff
|
namespace ai::buff
|
||||||
{
|
{
|
||||||
|
|
||||||
// Build an aura qualifier "single + greater" to avoid double-buffing
|
bool IsGroupVariantEnabled(Player* bot, std::string const& name);
|
||||||
|
|
||||||
std::string MakeAuraQualifierForBuff(std::string const& name);
|
std::string MakeAuraQualifierForBuff(std::string const& name);
|
||||||
|
|
||||||
// Returns the group spell name for a given single-target buff.
|
|
||||||
// If no group equivalent exists, returns "".
|
|
||||||
std::string GroupVariantFor(std::string const& name);
|
std::string GroupVariantFor(std::string const& name);
|
||||||
|
|
||||||
// Checks if the bot has the required reagents to cast a spell (by its spellId).
|
bool NeedsPostLoginBuffGrace(std::string const& name);
|
||||||
// Returns false if the spellId is invalid.
|
|
||||||
|
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
|
||||||
|
Player* bot,
|
||||||
|
Unit* target,
|
||||||
|
std::string const& spell);
|
||||||
|
|
||||||
|
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot);
|
||||||
|
|
||||||
bool HasRequiredReagents(Player* bot, uint32 spellId);
|
bool HasRequiredReagents(Player* bot, uint32 spellId);
|
||||||
|
|
||||||
// Applies the "switch to group buff" policy if: the bot is in a group of size x+,
|
|
||||||
// the group variant is known/useful, and reagents are available. Otherwise, returns baseName.
|
|
||||||
// If announceOnMissing == true and reagents are missing, calls the 'announce' callback
|
|
||||||
// (if provided) to notify the party/raid.
|
|
||||||
std::string UpgradeToGroupIfAppropriate(
|
std::string UpgradeToGroupIfAppropriate(
|
||||||
Player* bot,
|
Player* bot,
|
||||||
PlayerbotAI* botAI,
|
PlayerbotAI* botAI,
|
||||||
std::string const& baseName,
|
std::string const& baseName);
|
||||||
bool announceOnMissing = false,
|
|
||||||
std::function<void(std::string const&)> announce = {}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ai::spell
|
namespace ai::spell
|
||||||
{
|
{
|
||||||
bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId);
|
bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ai::chat {
|
|
||||||
inline std::function<void(std::string const&)> MakeGroupAnnouncer(Player* me)
|
|
||||||
{
|
|
||||||
return [me](std::string const& msg)
|
|
||||||
{
|
|
||||||
if (Group* g = me->GetGroup())
|
|
||||||
{
|
|
||||||
WorldPacket data;
|
|
||||||
ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY;
|
|
||||||
ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str());
|
|
||||||
g->BroadcastPacket(&data, true, -1, me->GetGUID());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
me->Say(msg, LANG_UNIVERSAL);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -47,7 +47,13 @@ public:
|
|||||||
{
|
{
|
||||||
if (allowedGOFlags.empty())
|
if (allowedGOFlags.empty())
|
||||||
{
|
{
|
||||||
|
// questgivers for accept/turn-in; rest for quest progression
|
||||||
|
// (chests, runes, altars, moonwells, lily piles, …)
|
||||||
allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER);
|
allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER);
|
||||||
|
allowedGOFlags.push_back(GAMEOBJECT_TYPE_CHEST);
|
||||||
|
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GOOBER);
|
||||||
|
allowedGOFlags.push_back(GAMEOBJECT_TYPE_SPELL_FOCUS);
|
||||||
|
allowedGOFlags.push_back(GAMEOBJECT_TYPE_GENERIC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,16 +87,16 @@ public:
|
|||||||
bool isUseful() override;
|
bool isUseful() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastMarkOfTheWildAction : public CastBuffSpellAction
|
class CastMarkOfTheWildAction : public GroupBuffSpellAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastMarkOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mark of the wild") {}
|
CastMarkOfTheWildAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "mark of the wild") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastMarkOfTheWildOnPartyAction : public BuffOnPartyAction
|
class CastMarkOfTheWildOnPartyAction : public GroupBuffOnPartyAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {}
|
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastSurvivalInstinctsAction : public CastBuffSpellAction
|
class CastSurvivalInstinctsAction : public CastBuffSpellAction
|
||||||
|
|||||||
@ -9,11 +9,6 @@
|
|||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
|
|
||||||
bool MarkOfTheWildOnPartyTrigger::IsActive()
|
|
||||||
{
|
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MarkOfTheWildTrigger::IsActive()
|
bool MarkOfTheWildTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
|
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
|
||||||
|
|||||||
@ -23,15 +23,13 @@ class PlayerbotAI;
|
|||||||
class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger
|
class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {}
|
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {}
|
||||||
|
|
||||||
bool IsActive() override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class MarkOfTheWildTrigger : public BuffTrigger
|
class MarkOfTheWildTrigger : public BuffTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 2 * 2000) {}
|
MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 4 * 2000) {}
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -40,16 +40,16 @@ public:
|
|||||||
CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {}
|
CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastArcaneIntellectAction : public CastBuffSpellAction
|
class CastArcaneIntellectAction : public GroupBuffSpellAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {}
|
CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction
|
class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {}
|
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastFocusMagicOnPartyAction : public CastSpellAction
|
class CastFocusMagicOnPartyAction : public CastSpellAction
|
||||||
|
|||||||
@ -31,11 +31,6 @@ bool NoManaGemTrigger::IsActive()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ArcaneIntellectOnPartyTrigger::IsActive()
|
|
||||||
{
|
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ArcaneIntellectTrigger::IsActive()
|
bool ArcaneIntellectTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
|
return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
|
||||||
|
|||||||
@ -19,14 +19,13 @@ class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI)
|
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {}
|
: BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {}
|
||||||
bool IsActive() override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ArcaneIntellectTrigger : public BuffTrigger
|
class ArcaneIntellectTrigger : public BuffTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {}
|
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {}
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,24 +7,100 @@
|
|||||||
|
|
||||||
#include "AiFactory.h"
|
#include "AiFactory.h"
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
#include "GenericBuffUtils.h"
|
||||||
|
#include "PaladinGreaterBlessingAction.h"
|
||||||
#include "PaladinHelper.h"
|
#include "PaladinHelper.h"
|
||||||
#include "PlayerbotAI.h"
|
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
#include "../../../../../src/server/scripts/Spells/spell_generic.cpp"
|
|
||||||
#include "Ai/Base/Util/GenericBuffUtils.h"
|
|
||||||
#include "Group.h"
|
|
||||||
#include "ObjectAccessor.h"
|
|
||||||
|
|
||||||
using ai::buff::MakeAuraQualifierForBuff;
|
static bool IsBlessingTargetCandidate(Player* bot, Player* player)
|
||||||
|
|
||||||
// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face).
|
|
||||||
static inline bool IsTankRole(Player* p)
|
|
||||||
{
|
{
|
||||||
if (!p) return false;
|
if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId())
|
||||||
if (p->HasTankSpec())
|
return false;
|
||||||
|
|
||||||
|
if (player->IsGameMaster())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 &&
|
||||||
|
bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(),
|
||||||
|
player->GetPositionZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasBlessingAura(
|
||||||
|
PlayerbotAI* botAI, Unit* target, std::initializer_list<char const*> auraNames)
|
||||||
|
{
|
||||||
|
for (char const* auraName : auraNames)
|
||||||
|
{
|
||||||
|
if (botAI->HasAura(auraName, target))
|
||||||
return true;
|
return true;
|
||||||
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p))
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsGreaterBlessingMode(Player* bot)
|
||||||
|
{
|
||||||
|
return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Predicate>
|
||||||
|
static Unit* FindBlessingTarget(
|
||||||
|
Player* bot, PlayerbotAI* botAI, Predicate&& predicate)
|
||||||
|
{
|
||||||
|
std::vector<Player*> masters;
|
||||||
|
std::vector<Player*> healers;
|
||||||
|
std::vector<Player*> tanks;
|
||||||
|
std::vector<Player*> others;
|
||||||
|
|
||||||
|
Player* master = botAI->GetMaster();
|
||||||
|
auto addPlayer = [&](Player* player)
|
||||||
|
{
|
||||||
|
if (!IsBlessingTargetCandidate(bot, player))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (player == master)
|
||||||
|
masters.push_back(player);
|
||||||
|
else if (botAI->IsHeal(player))
|
||||||
|
healers.push_back(player);
|
||||||
|
else if (botAI->IsTank(player))
|
||||||
|
tanks.push_back(player);
|
||||||
|
else
|
||||||
|
others.push_back(player);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Group* group = bot->GetGroup())
|
||||||
|
{
|
||||||
|
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||||
|
addPlayer(ref->GetSource());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addPlayer(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<Player*>*> orderedLists = {
|
||||||
|
&masters, &healers, &tanks, &others };
|
||||||
|
for (std::vector<Player*>* players : orderedLists)
|
||||||
|
{
|
||||||
|
for (Player* player : *players)
|
||||||
|
{
|
||||||
|
if (predicate(player))
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool IsTankRole(Player* player)
|
||||||
|
{
|
||||||
|
if (!player)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (player->HasTankSpec())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player))
|
||||||
{
|
{
|
||||||
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
|
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
|
||||||
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
|
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
|
||||||
@ -34,33 +110,36 @@ static inline bool IsTankRole(Player* p)
|
|||||||
otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
|
otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added for solo paladin patch : determine if he's the only paladin on party
|
|
||||||
static inline bool IsOnlyPaladinInGroup(Player* bot)
|
static inline bool IsOnlyPaladinInGroup(Player* bot)
|
||||||
{
|
{
|
||||||
if (!bot) return false;
|
if (!bot)
|
||||||
Group* g = bot->GetGroup();
|
return false;
|
||||||
if (!g) return true; // solo
|
|
||||||
uint32 pals = 0u;
|
Group* group = bot->GetGroup();
|
||||||
for (GroupReference* r = g->GetFirstMember(); r; r = r->next())
|
if (!group)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
uint32 paladins = 0u;
|
||||||
|
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||||
{
|
{
|
||||||
Player* p = r->GetSource();
|
Player* player = ref->GetSource();
|
||||||
if (!p || !p->IsInWorld()) continue;
|
if (!player || !player->IsInWorld()) continue;
|
||||||
if (p->getClass() == CLASS_PALADIN) ++pals;
|
if (player->getClass() == CLASS_PALADIN) ++paladins;
|
||||||
}
|
}
|
||||||
return pals == 1u;
|
|
||||||
|
return paladins == 1u;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::string const GetActualBlessingOfMight(Unit* target)
|
inline std::string const GetActualBlessingOfMight(Unit* target)
|
||||||
{
|
{
|
||||||
if (!target->ToPlayer())
|
if (!target->ToPlayer())
|
||||||
{
|
|
||||||
return "blessing of might";
|
return "blessing of might";
|
||||||
}
|
|
||||||
|
|
||||||
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
|
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
|
||||||
switch (target->getClass())
|
switch (target->getClass())
|
||||||
{
|
{
|
||||||
case CLASS_MAGE:
|
case CLASS_MAGE:
|
||||||
@ -70,21 +149,15 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
|
|||||||
break;
|
break;
|
||||||
case CLASS_SHAMAN:
|
case CLASS_SHAMAN:
|
||||||
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
|
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
|
||||||
{
|
|
||||||
return "blessing of wisdom";
|
return "blessing of wisdom";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case CLASS_DRUID:
|
case CLASS_DRUID:
|
||||||
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
|
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
|
||||||
{
|
|
||||||
return "blessing of wisdom";
|
return "blessing of wisdom";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case CLASS_PALADIN:
|
case CLASS_PALADIN:
|
||||||
if (tab == PALADIN_TAB_HOLY)
|
if (tab == PALADIN_TAB_HOLY)
|
||||||
{
|
|
||||||
return "blessing of wisdom";
|
return "blessing of wisdom";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,10 +167,9 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
|
|||||||
inline std::string const GetActualBlessingOfWisdom(Unit* target)
|
inline std::string const GetActualBlessingOfWisdom(Unit* target)
|
||||||
{
|
{
|
||||||
if (!target->ToPlayer())
|
if (!target->ToPlayer())
|
||||||
{
|
|
||||||
return "blessing of might";
|
return "blessing of might";
|
||||||
}
|
|
||||||
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
|
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
|
||||||
switch (target->getClass())
|
switch (target->getClass())
|
||||||
{
|
{
|
||||||
case CLASS_WARRIOR:
|
case CLASS_WARRIOR:
|
||||||
@ -108,21 +180,15 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
|
|||||||
break;
|
break;
|
||||||
case CLASS_SHAMAN:
|
case CLASS_SHAMAN:
|
||||||
if (tab == SHAMAN_TAB_ENHANCEMENT)
|
if (tab == SHAMAN_TAB_ENHANCEMENT)
|
||||||
{
|
|
||||||
return "blessing of might";
|
return "blessing of might";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case CLASS_DRUID:
|
case CLASS_DRUID:
|
||||||
if (tab == DRUID_TAB_FERAL)
|
if (tab == DRUID_TAB_FERAL)
|
||||||
{
|
|
||||||
return "blessing of might";
|
return "blessing of might";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case CLASS_PALADIN:
|
case CLASS_PALADIN:
|
||||||
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
|
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
|
||||||
{
|
|
||||||
return "blessing of might";
|
return "blessing of might";
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,32 +197,41 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
|
|||||||
|
|
||||||
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
|
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
|
||||||
{
|
{
|
||||||
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
|
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
Player* tp = target->ToPlayer();
|
Player* targetPlayer = target->ToPlayer();
|
||||||
if (!tp)
|
if (!targetPlayer)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
if (auto* ai = GET_PLAYERBOT_AI(bot))
|
if (auto* botAI = GET_PLAYERBOT_AI(bot))
|
||||||
{
|
{
|
||||||
if (Unit* mt = ai->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
|
if (Unit* mainTank =
|
||||||
|
botAI->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
|
||||||
{
|
{
|
||||||
if (mt == target)
|
if (mainTank == target)
|
||||||
return "blessing of sanctuary";
|
return "blessing of sanctuary";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tp->HasTankSpec())
|
if (targetPlayer->HasTankSpec())
|
||||||
return "blessing of sanctuary";
|
return "blessing of sanctuary";
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Value<Unit*>* CastBlessingOnPartyAction::GetTargetValue()
|
Unit* CastBlessingOfMightOnPartyAction::GetTarget()
|
||||||
{
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
|
return FindBlessingTarget(bot, botAI, [&](Player* player)
|
||||||
|
{
|
||||||
|
return !HasBlessingAura(botAI, player,
|
||||||
|
{ "blessing of might", "greater blessing of might",
|
||||||
|
"blessing of wisdom", "greater blessing of wisdom",
|
||||||
|
"blessing of sanctuary", "greater blessing of sanctuary" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CastBlessingOfMightAction::Execute(Event /*event*/)
|
bool CastBlessingOfMightAction::Execute(Event /*event*/)
|
||||||
@ -166,9 +241,6 @@ bool CastBlessingOfMightAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::string castName = GetActualBlessingOfMight(target);
|
std::string castName = GetActualBlessingOfMight(target);
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
return botAI->CastSpell(castName, target);
|
return botAI->CastSpell(castName, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,20 +248,22 @@ Value<Unit*>* CastBlessingOfMightOnPartyAction::GetTargetValue()
|
|||||||
{
|
{
|
||||||
return context->GetValue<Unit*>(
|
return context->GetValue<Unit*>(
|
||||||
"party member without aura",
|
"party member without aura",
|
||||||
"blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary"
|
"blessing of might,greater blessing of might,blessing of wisdom,"
|
||||||
|
"greater blessing of wisdom,blessing of sanctuary,"
|
||||||
|
"greater blessing of sanctuary"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
|
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return false;
|
||||||
|
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::string castName = GetActualBlessingOfMight(target);
|
std::string castName = GetActualBlessingOfMight(target);
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
return botAI->CastSpell(castName, target);
|
return botAI->CastSpell(castName, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,45 +274,58 @@ bool CastBlessingOfWisdomAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::string castName = GetActualBlessingOfWisdom(target);
|
std::string castName = GetActualBlessingOfWisdom(target);
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
return botAI->CastSpell(castName, target);
|
return botAI->CastSpell(castName, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Unit* CastBlessingOfWisdomOnPartyAction::GetTarget()
|
||||||
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return FindBlessingTarget(bot, botAI, [&](Player* player)
|
||||||
|
{
|
||||||
|
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !HasBlessingAura(botAI, player,
|
||||||
|
{ "blessing of might", "greater blessing of might",
|
||||||
|
"blessing of wisdom", "greater blessing of wisdom",
|
||||||
|
"blessing of sanctuary", "greater blessing of sanctuary" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
|
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
|
||||||
{
|
{
|
||||||
return context->GetValue<Unit*>(
|
return context->GetValue<Unit*>(
|
||||||
"party member without aura",
|
"party member without aura",
|
||||||
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary"
|
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,"
|
||||||
|
"blessing of sanctuary,greater blessing of sanctuary"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
|
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return false;
|
||||||
|
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Player* targetPlayer = target->ToPlayer();
|
Player* targetPlayer = target->ToPlayer();
|
||||||
|
|
||||||
if (Group* g = bot->GetGroup())
|
if (Group* group = bot->GetGroup())
|
||||||
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
|
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) &&
|
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) &&
|
||||||
targetPlayer && IsTankRole(targetPlayer))
|
targetPlayer && IsTankRole(targetPlayer))
|
||||||
{
|
|
||||||
LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
std::string castName = GetActualBlessingOfWisdom(target);
|
std::string castName = GetActualBlessingOfWisdom(target);
|
||||||
if (castName.empty())
|
if (castName.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
return botAI->CastSpell(castName, target);
|
return botAI->CastSpell(castName, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,32 +339,31 @@ Value<Unit*>* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue()
|
|||||||
|
|
||||||
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target)
|
||||||
{
|
|
||||||
// Fallback: GetTarget() can be null if no one needs a buff.
|
|
||||||
// Keep a valid pointer for the checks/logs that follow.
|
|
||||||
target = bot;
|
target = bot;
|
||||||
}
|
|
||||||
|
|
||||||
Player* targetPlayer = target ? target->ToPlayer() : nullptr;
|
Player* targetPlayer = target ? target->ToPlayer() : nullptr;
|
||||||
|
|
||||||
// Small helpers to check relevant auras
|
const auto HasKingsAura = [&](Unit* unit) -> bool {
|
||||||
const auto HasKingsAura = [&](Unit* u) -> bool {
|
return botAI->HasAura("blessing of kings", unit) ||
|
||||||
return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u);
|
botAI->HasAura("greater blessing of kings", unit);
|
||||||
};
|
};
|
||||||
const auto HasSanctAura = [&](Unit* u) -> bool {
|
const auto HasSanctAura = [&](Unit* unit) -> bool {
|
||||||
return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u);
|
return botAI->HasAura("blessing of sanctuary", unit) ||
|
||||||
|
botAI->HasAura("greater blessing of sanctuary", unit);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Group* g = bot->GetGroup())
|
if (Group* group = bot->GetGroup())
|
||||||
{
|
{
|
||||||
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
|
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
|
||||||
{
|
{
|
||||||
LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring");
|
|
||||||
target = bot;
|
target = bot;
|
||||||
targetPlayer = bot->ToPlayer();
|
targetPlayer = bot->ToPlayer();
|
||||||
}
|
}
|
||||||
@ -288,9 +374,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
|||||||
bool selfHasSanct = HasSanctAura(self);
|
bool selfHasSanct = HasSanctAura(self);
|
||||||
bool needSelf = IsTankRole(self) && !selfHasSanct;
|
bool needSelf = IsTankRole(self) && !selfHasSanct;
|
||||||
|
|
||||||
LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}",
|
|
||||||
bot->GetName(), IsTankRole(self), selfHasSanct, needSelf);
|
|
||||||
|
|
||||||
if (needSelf)
|
if (needSelf)
|
||||||
{
|
{
|
||||||
target = self;
|
target = self;
|
||||||
@ -298,7 +381,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to re-target a valid tank in group if needed
|
|
||||||
bool targetOk = false;
|
bool targetOk = false;
|
||||||
if (targetPlayer)
|
if (targetPlayer)
|
||||||
{
|
{
|
||||||
@ -308,20 +390,20 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
if (!targetOk)
|
if (!targetOk)
|
||||||
{
|
{
|
||||||
if (Group* g = bot->GetGroup())
|
if (Group* group = bot->GetGroup())
|
||||||
{
|
{
|
||||||
for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next())
|
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
|
||||||
{
|
{
|
||||||
Player* p = gref->GetSource();
|
Player* player = ref->GetSource();
|
||||||
if (!p) continue;
|
if (!player) continue;
|
||||||
if (!p->IsInWorld() || !p->IsAlive()) continue;
|
if (!player->IsInWorld() || !player->IsAlive()) continue;
|
||||||
if (!IsTankRole(p)) continue;
|
if (!IsTankRole(player)) continue;
|
||||||
|
|
||||||
bool hasSanct = HasSanctAura(p);
|
bool hasSanct = HasSanctAura(player);
|
||||||
if (!hasSanct)
|
if (!hasSanct)
|
||||||
{
|
{
|
||||||
target = p; // prioritize this tank
|
target = player;
|
||||||
targetPlayer = p;
|
targetPlayer = player;
|
||||||
targetOk = true;
|
targetOk = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -329,150 +411,147 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if (GetActualBlessingOfSanctuary(target, bot).empty())
|
||||||
bool hasKings = HasKingsAura(target);
|
|
||||||
bool hasSanct = HasSanctAura(target);
|
|
||||||
bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY);
|
|
||||||
LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}",
|
|
||||||
target->GetName(), hasKings, hasSanct, knowSanct);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string castName = GetActualBlessingOfSanctuary(target, bot);
|
|
||||||
// If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary
|
|
||||||
if (castName.empty())
|
|
||||||
{
|
{
|
||||||
if (targetPlayer)
|
if (targetPlayer)
|
||||||
{
|
{
|
||||||
if (IsTankRole(targetPlayer))
|
if (IsTankRole(targetPlayer))
|
||||||
castName = "blessing of sanctuary"; // force single-target
|
return botAI->CastSpell("blessing of sanctuary", target);
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (targetPlayer && !IsTankRole(targetPlayer))
|
|
||||||
{
|
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
castName = "blessing of sanctuary";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = botAI->CastSpell(castName, target);
|
return botAI->CastSpell("blessing of sanctuary", target);
|
||||||
LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok);
|
}
|
||||||
return ok;
|
|
||||||
|
Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget()
|
||||||
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return FindBlessingTarget(bot, botAI, [&](Player* player)
|
||||||
|
{
|
||||||
|
return IsTankRole(player) &&
|
||||||
|
!HasBlessingAura(botAI, player,
|
||||||
|
{ "blessing of sanctuary", "greater blessing of sanctuary" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
|
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
|
||||||
{
|
{
|
||||||
return context->GetValue<Unit*>(
|
return context->GetValue<Unit*>(
|
||||||
"party member without aura",
|
"party member without aura",
|
||||||
"blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary"
|
"blessing of kings,greater blessing of kings,"
|
||||||
|
"blessing of sanctuary,greater blessing of sanctuary"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Unit* CastBlessingOfKingsOnPartyAction::GetTarget()
|
||||||
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
|
||||||
|
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
|
||||||
|
const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot);
|
||||||
|
|
||||||
|
return FindBlessingTarget(bot, botAI, [&](Player* player)
|
||||||
|
{
|
||||||
|
const bool isTank = IsTankRole(player);
|
||||||
|
const bool hasKingsOrSanct = HasBlessingAura(botAI, player,
|
||||||
|
{ "blessing of kings", "greater blessing of kings",
|
||||||
|
"blessing of sanctuary", "greater blessing of sanctuary" });
|
||||||
|
|
||||||
|
if (hasKingsOrSanct)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (hasBwisdom)
|
||||||
|
return isTank;
|
||||||
|
|
||||||
|
if (hasBkings)
|
||||||
|
{
|
||||||
|
if (isTank)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (onlyPaladinInGroup && player == bot)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
|
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
|
||||||
{
|
{
|
||||||
|
if (IsGreaterBlessingMode(bot))
|
||||||
|
return false;
|
||||||
|
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Group* g = bot->GetGroup();
|
Group* group = bot->GetGroup();
|
||||||
if (!g)
|
if (!group)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Added for patch solo paladin, never buff itself to not remove his sanctuary buff
|
if (botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT) &&
|
||||||
if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot))
|
IsOnlyPaladinInGroup(bot))
|
||||||
{
|
{
|
||||||
if (target->GetGUID() == bot->GetGUID())
|
if (target->GetGUID() == bot->GetGUID())
|
||||||
{
|
|
||||||
LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// End solo paladin patch
|
|
||||||
|
|
||||||
Player* targetPlayer = target->ToPlayer();
|
Player* targetPlayer = target->ToPlayer();
|
||||||
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
|
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT);
|
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
|
||||||
const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT);
|
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
|
||||||
|
|
||||||
if (hasBmana)
|
if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer)))
|
||||||
{
|
|
||||||
if (!targetPlayer || !IsTankRole(targetPlayer))
|
|
||||||
{
|
|
||||||
LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetPlayer)
|
if (targetPlayer)
|
||||||
{
|
{
|
||||||
const bool isTank = IsTankRole(targetPlayer);
|
const bool isTank = IsTankRole(targetPlayer);
|
||||||
const bool hasSanctFromMe =
|
const bool hasSanctFromMe =
|
||||||
target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
|
target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
|
||||||
target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
|
target->HasAura(ai::paladin::SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
|
||||||
const bool hasSanctAny =
|
const bool hasSanctAny =
|
||||||
botAI->HasAura("blessing of sanctuary", target) ||
|
botAI->HasAura("blessing of sanctuary", target) ||
|
||||||
botAI->HasAura("greater blessing of sanctuary", target);
|
botAI->HasAura("greater blessing of sanctuary", target);
|
||||||
|
|
||||||
if (isTank && hasSanctFromMe)
|
if (isTank && hasSanctFromMe)
|
||||||
{
|
return false;
|
||||||
LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName());
|
|
||||||
|
if (hasBkings && isTank && hasSanctAny)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBstats && isTank && hasSanctAny)
|
return botAI->CastSpell("blessing of kings", target);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CastSealSpellAction::isUseful()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName());
|
return AI_VALUE2(bool, "combat", "self target");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string castName = "blessing of kings";
|
Value<Unit*>* CastTurnUndeadAction::GetTargetValue()
|
||||||
|
|
||||||
bool allowGreater = true;
|
|
||||||
|
|
||||||
if (hasBmana)
|
|
||||||
allowGreater = false;
|
|
||||||
|
|
||||||
if (allowGreater && hasBstats && targetPlayer)
|
|
||||||
{
|
{
|
||||||
switch (targetPlayer->getClass())
|
return context->GetValue<Unit*>("cc target", getName());
|
||||||
{
|
|
||||||
case CLASS_WARRIOR:
|
|
||||||
case CLASS_PALADIN:
|
|
||||||
case CLASS_DRUID:
|
|
||||||
case CLASS_DEATH_KNIGHT:
|
|
||||||
allowGreater = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (allowGreater)
|
|
||||||
{
|
|
||||||
auto RP = ai::chat::MakeGroupAnnouncer(bot);
|
|
||||||
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
|
|
||||||
}
|
|
||||||
|
|
||||||
return botAI->CastSpell(castName, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); }
|
|
||||||
|
|
||||||
Value<Unit*>* CastTurnUndeadAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", getName()); }
|
|
||||||
|
|
||||||
Unit* CastHandOfFreedomOnPartyAction::GetTarget()
|
Unit* CastHandOfFreedomOnPartyAction::GetTarget()
|
||||||
{
|
{
|
||||||
bool const selfImpaired = botAI->IsMovementImpaired(bot);
|
bool const selfImpaired = botAI->IsMovementImpaired(bot);
|
||||||
bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
|
bool const hasSelfHand =
|
||||||
|
selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
|
||||||
|
|
||||||
if (!bot->GetGroup())
|
if (!bot->GetGroup())
|
||||||
{
|
{
|
||||||
@ -499,7 +578,8 @@ bool CastHandOfFreedomOnPartyAction::isUseful()
|
|||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
|
return CastBuffSpellAction::isUseful() &&
|
||||||
|
!ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
Unit* CastRighteousDefenseAction::GetTarget()
|
Unit* CastRighteousDefenseAction::GetTarget()
|
||||||
|
|||||||
@ -8,10 +8,6 @@
|
|||||||
|
|
||||||
#include "AiObject.h"
|
#include "AiObject.h"
|
||||||
#include "GenericSpellActions.h"
|
#include "GenericSpellActions.h"
|
||||||
#include "SharedDefines.h"
|
|
||||||
|
|
||||||
class PlayerbotAI;
|
|
||||||
class Unit;
|
|
||||||
|
|
||||||
// seals
|
// seals
|
||||||
BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness");
|
BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness");
|
||||||
@ -88,24 +84,13 @@ public:
|
|||||||
bool Execute(Event event) override;
|
bool Execute(Event event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastBlessingOnPartyAction : public BuffOnPartyAction
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name)
|
|
||||||
: BuffOnPartyAction(botAI, name), name(name) {}
|
|
||||||
|
|
||||||
Value<Unit*>* GetTargetValue() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::string name;
|
|
||||||
};
|
|
||||||
|
|
||||||
class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction
|
class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {}
|
CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {}
|
||||||
|
|
||||||
std::string const getName() override { return "blessing of might on party"; }
|
std::string const getName() override { return "blessing of might on party"; }
|
||||||
|
Unit* GetTarget() override;
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
bool Execute(Event event) override;
|
bool Execute(Event event) override;
|
||||||
};
|
};
|
||||||
@ -124,6 +109,7 @@ public:
|
|||||||
CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {}
|
CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {}
|
||||||
|
|
||||||
std::string const getName() override { return "blessing of wisdom on party"; }
|
std::string const getName() override { return "blessing of wisdom on party"; }
|
||||||
|
Unit* GetTarget() override;
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
bool Execute(Event event) override;
|
bool Execute(Event event) override;
|
||||||
};
|
};
|
||||||
@ -134,12 +120,13 @@ public:
|
|||||||
CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {}
|
CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction
|
class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {}
|
CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of kings") {}
|
||||||
|
|
||||||
std::string const getName() override { return "blessing of kings on party"; }
|
std::string const getName() override { return "blessing of kings on party"; }
|
||||||
|
Unit* GetTarget() override;
|
||||||
Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority
|
Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority
|
||||||
bool Execute(Event event) override; // added for 2 paladins logic
|
bool Execute(Event event) override; // added for 2 paladins logic
|
||||||
};
|
};
|
||||||
@ -156,6 +143,7 @@ public:
|
|||||||
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
|
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
|
||||||
|
|
||||||
std::string const getName() override { return "blessing of sanctuary on party"; }
|
std::string const getName() override { return "blessing of sanctuary on party"; }
|
||||||
|
Unit* GetTarget() override;
|
||||||
Value<Unit*>* GetTargetValue() override;
|
Value<Unit*>* GetTargetValue() override;
|
||||||
bool Execute(Event event) override;
|
bool Execute(Event event) override;
|
||||||
};
|
};
|
||||||
|
|||||||
1116
src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.cpp
Normal file
1116
src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.cpp
Normal file
File diff suppressed because it is too large
Load Diff
267
src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.h
Normal file
267
src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.h
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
||||||
|
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _PLAYERBOT_PALADINGREATERBLESSINGACTION_H
|
||||||
|
#define _PLAYERBOT_PALADINGREATERBLESSINGACTION_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Action.h"
|
||||||
|
#include "AiFactory.h"
|
||||||
|
#include "Playerbots.h"
|
||||||
|
#include "SharedDefines.h"
|
||||||
|
|
||||||
|
class UntypedValue;
|
||||||
|
|
||||||
|
namespace ai::gbless
|
||||||
|
{
|
||||||
|
enum RoleProfile : uint8
|
||||||
|
{
|
||||||
|
ROLE_CASTER = 0,
|
||||||
|
ROLE_PHYSICAL_DPS = 1,
|
||||||
|
ROLE_HYBRID_DPS = 2,
|
||||||
|
ROLE_DRUID_TANK = 3,
|
||||||
|
ROLE_WARRIOR_DK_TANK = 4,
|
||||||
|
ROLE_PALADIN_TANK = 5,
|
||||||
|
|
||||||
|
ROLE_PROFILE_COUNT = 6
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BlessingType : uint8
|
||||||
|
{
|
||||||
|
BLESSING_NONE = 0,
|
||||||
|
BLESSING_MIGHT_SINGLE = 1,
|
||||||
|
BLESSING_MIGHT_GREATER = 2,
|
||||||
|
BLESSING_WISDOM_SINGLE = 3,
|
||||||
|
BLESSING_WISDOM_GREATER = 4,
|
||||||
|
BLESSING_KINGS_SINGLE = 5,
|
||||||
|
BLESSING_KINGS_GREATER = 6,
|
||||||
|
BLESSING_SANCTUARY_SINGLE = 7,
|
||||||
|
BLESSING_SANCTUARY_GREATER = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BaseBlessingCategory : uint8
|
||||||
|
{
|
||||||
|
BASE_NONE = 0,
|
||||||
|
BASE_MIGHT = 1,
|
||||||
|
BASE_WISDOM = 2,
|
||||||
|
BASE_KINGS = 3,
|
||||||
|
BASE_SANCTUARY = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr BaseBlessingCategory BaseBlessingOf(BlessingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BLESSING_MIGHT_SINGLE:
|
||||||
|
case BLESSING_MIGHT_GREATER: return BASE_MIGHT;
|
||||||
|
case BLESSING_WISDOM_SINGLE:
|
||||||
|
case BLESSING_WISDOM_GREATER: return BASE_WISDOM;
|
||||||
|
case BLESSING_KINGS_SINGLE:
|
||||||
|
case BLESSING_KINGS_GREATER: return BASE_KINGS;
|
||||||
|
case BLESSING_SANCTUARY_SINGLE:
|
||||||
|
case BLESSING_SANCTUARY_GREATER: return BASE_SANCTUARY;
|
||||||
|
default: return BASE_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr bool IsSingleVariant(BlessingType type)
|
||||||
|
{
|
||||||
|
return type == BLESSING_MIGHT_SINGLE || type == BLESSING_WISDOM_SINGLE ||
|
||||||
|
type == BLESSING_KINGS_SINGLE || type == BLESSING_SANCTUARY_SINGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr bool IsGreaterVariant(BlessingType type)
|
||||||
|
{
|
||||||
|
return type == BLESSING_MIGHT_GREATER || type == BLESSING_WISDOM_GREATER ||
|
||||||
|
type == BLESSING_KINGS_GREATER || type == BLESSING_SANCTUARY_GREATER;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr BlessingType ToSingleVariant(BaseBlessingCategory category)
|
||||||
|
{
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case BASE_MIGHT: return BLESSING_MIGHT_SINGLE;
|
||||||
|
case BASE_WISDOM: return BLESSING_WISDOM_SINGLE;
|
||||||
|
case BASE_KINGS: return BLESSING_KINGS_SINGLE;
|
||||||
|
case BASE_SANCTUARY: return BLESSING_SANCTUARY_SINGLE;
|
||||||
|
default: return BLESSING_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr BlessingType ToSingleVariant(BlessingType type)
|
||||||
|
{
|
||||||
|
return ToSingleVariant(BaseBlessingOf(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr BlessingType ToGreaterVariant(BaseBlessingCategory category)
|
||||||
|
{
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case BASE_MIGHT: return BLESSING_MIGHT_GREATER;
|
||||||
|
case BASE_WISDOM: return BLESSING_WISDOM_GREATER;
|
||||||
|
case BASE_KINGS: return BLESSING_KINGS_GREATER;
|
||||||
|
case BASE_SANCTUARY: return BLESSING_SANCTUARY_GREATER;
|
||||||
|
default: return BLESSING_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr BlessingType ToGreaterVariant(BlessingType type)
|
||||||
|
{
|
||||||
|
return ToGreaterVariant(BaseBlessingOf(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string BlessingSpellName(BlessingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BLESSING_MIGHT_SINGLE: return "blessing of might";
|
||||||
|
case BLESSING_MIGHT_GREATER: return "greater blessing of might";
|
||||||
|
case BLESSING_WISDOM_SINGLE: return "blessing of wisdom";
|
||||||
|
case BLESSING_WISDOM_GREATER: return "greater blessing of wisdom";
|
||||||
|
case BLESSING_KINGS_SINGLE: return "blessing of kings";
|
||||||
|
case BLESSING_KINGS_GREATER: return "greater blessing of kings";
|
||||||
|
case BLESSING_SANCTUARY_SINGLE: return "blessing of sanctuary";
|
||||||
|
case BLESSING_SANCTUARY_GREATER: return "greater blessing of sanctuary";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BaseBlessingPriorityEntry
|
||||||
|
{
|
||||||
|
BaseBlessingCategory priorities[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr BaseBlessingPriorityEntry BASE_BLESSING_PRIORITIES[ROLE_PROFILE_COUNT] =
|
||||||
|
{
|
||||||
|
// All casters
|
||||||
|
{{ BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY, BASE_MIGHT }},
|
||||||
|
// Physical DPS (no mana)
|
||||||
|
{{ BASE_MIGHT, BASE_KINGS, BASE_SANCTUARY, BASE_NONE }},
|
||||||
|
// Hybrid DPS
|
||||||
|
{{ BASE_MIGHT, BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY }},
|
||||||
|
// Druid tanks
|
||||||
|
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_WISDOM, }},
|
||||||
|
// Warrior and DK tanks
|
||||||
|
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_NONE }},
|
||||||
|
// Paladin tanks
|
||||||
|
{{ BASE_SANCTUARY, BASE_MIGHT, BASE_WISDOM, BASE_KINGS }},
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr uint32 SPELL_IMPROVED_MIGHT_R1 = 20042;
|
||||||
|
constexpr uint32 SPELL_IMPROVED_MIGHT_R2 = 20045;
|
||||||
|
constexpr uint32 SPELL_IMPROVED_WISDOM_R1 = 20244;
|
||||||
|
constexpr uint32 SPELL_IMPROVED_WISDOM_R2 = 20245;
|
||||||
|
|
||||||
|
inline RoleProfile ResolveRoleProfile(Player* player)
|
||||||
|
{
|
||||||
|
if (!player)
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
uint8 cls = player->getClass();
|
||||||
|
int tab = AiFactory::GetPlayerSpecTab(player);
|
||||||
|
bool isTank = PlayerbotAI::IsTank(player);
|
||||||
|
|
||||||
|
switch (cls)
|
||||||
|
{
|
||||||
|
case CLASS_WARRIOR:
|
||||||
|
if (isTank)
|
||||||
|
return ROLE_WARRIOR_DK_TANK;
|
||||||
|
return ROLE_PHYSICAL_DPS;
|
||||||
|
|
||||||
|
case CLASS_DEATH_KNIGHT:
|
||||||
|
if (isTank)
|
||||||
|
return ROLE_WARRIOR_DK_TANK;
|
||||||
|
return ROLE_PHYSICAL_DPS;
|
||||||
|
|
||||||
|
case CLASS_SHAMAN:
|
||||||
|
if (tab == SHAMAN_TAB_ENHANCEMENT)
|
||||||
|
return ROLE_HYBRID_DPS;
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
case CLASS_PALADIN:
|
||||||
|
if (isTank)
|
||||||
|
return ROLE_PALADIN_TANK;
|
||||||
|
if (tab == PALADIN_TAB_HOLY)
|
||||||
|
return ROLE_CASTER;
|
||||||
|
return ROLE_HYBRID_DPS;
|
||||||
|
|
||||||
|
case CLASS_DRUID:
|
||||||
|
if (tab == DRUID_TAB_FERAL)
|
||||||
|
return isTank ? ROLE_DRUID_TANK : ROLE_HYBRID_DPS;
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
case CLASS_ROGUE:
|
||||||
|
return ROLE_PHYSICAL_DPS;
|
||||||
|
|
||||||
|
case CLASS_HUNTER:
|
||||||
|
return ROLE_HYBRID_DPS;
|
||||||
|
|
||||||
|
case CLASS_MAGE:
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
case CLASS_WARLOCK:
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
case CLASS_PRIEST:
|
||||||
|
return ROLE_CASTER;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ROLE_CASTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GreaterBlessingPlayerAssignment
|
||||||
|
{
|
||||||
|
Player* player = nullptr;
|
||||||
|
BlessingType blessing = BLESSING_NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CachedBlessingBucketAssignment
|
||||||
|
{
|
||||||
|
uint8 classId = 0;
|
||||||
|
RoleProfile role = ROLE_CASTER;
|
||||||
|
bool byRole = false;
|
||||||
|
BlessingType blessing = BLESSING_NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CachedBlessingAssignments
|
||||||
|
{
|
||||||
|
uint32 groupKey = 0;
|
||||||
|
bool valid = false;
|
||||||
|
std::vector<CachedBlessingBucketAssignment> assignments;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CachedPendingBlessingAssignment
|
||||||
|
{
|
||||||
|
uint32 groupKey = 0;
|
||||||
|
bool valid = false;
|
||||||
|
GreaterBlessingPlayerAssignment assignment;
|
||||||
|
std::string spellName;
|
||||||
|
};
|
||||||
|
|
||||||
|
UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI);
|
||||||
|
UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI);
|
||||||
|
bool IsEligibleGroupForAutoBlessings(Group const* group);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastGreaterBlessingAssignmentAction : public Action
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CastGreaterBlessingAssignmentAction(PlayerbotAI* botAI);
|
||||||
|
|
||||||
|
bool Execute(Event event) override;
|
||||||
|
bool isUseful() override;
|
||||||
|
bool HasPendingAssignment();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool FindPendingAssignment(
|
||||||
|
ai::gbless::GreaterBlessingPlayerAssignment& outAssignment,
|
||||||
|
std::string& outSpellName);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "DpsPaladinStrategy.h"
|
#include "DpsPaladinStrategy.h"
|
||||||
#include "GenericPaladinNonCombatStrategy.h"
|
#include "GenericPaladinNonCombatStrategy.h"
|
||||||
|
#include "PaladinGreaterBlessingAction.h"
|
||||||
#include "HealPaladinStrategy.h"
|
#include "HealPaladinStrategy.h"
|
||||||
#include "NamedObjectContext.h"
|
#include "NamedObjectContext.h"
|
||||||
#include "OffhealRetPaladinStrategy.h"
|
#include "OffhealRetPaladinStrategy.h"
|
||||||
@ -70,17 +71,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
|
|||||||
public:
|
public:
|
||||||
PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
|
PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
|
||||||
{
|
{
|
||||||
creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth;
|
creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc;
|
||||||
creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana;
|
creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom;
|
||||||
creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps;
|
creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight;
|
||||||
creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats;
|
creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
|
static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
|
||||||
static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
|
static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
|
||||||
static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
|
static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
|
||||||
static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
|
static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
|
class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
|
||||||
@ -154,6 +155,7 @@ public:
|
|||||||
creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party;
|
creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party;
|
||||||
|
|
||||||
creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath;
|
creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath;
|
||||||
|
creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -211,8 +213,8 @@ private:
|
|||||||
static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); }
|
static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); }
|
||||||
static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); }
|
static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); }
|
||||||
static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); }
|
static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); }
|
||||||
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); }
|
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); }
|
||||||
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); }
|
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new SacredShieldOnMainTankTrigger(botAI); }
|
||||||
static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); }
|
static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); }
|
||||||
|
|
||||||
static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); }
|
static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); }
|
||||||
@ -227,6 +229,10 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); }
|
static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); }
|
||||||
|
static Trigger* greater_blessing_needed(PlayerbotAI* botAI)
|
||||||
|
{
|
||||||
|
return new GreaterBlessingNeededTrigger(botAI);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinAiObjectContextInternal : public NamedObjectContext<Action>
|
class PaladinAiObjectContextInternal : public NamedObjectContext<Action>
|
||||||
@ -316,6 +322,8 @@ public:
|
|||||||
creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice;
|
creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice;
|
||||||
creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice;
|
creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice;
|
||||||
creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party;
|
creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party;
|
||||||
|
creators["cast greater blessing assignment"] =
|
||||||
|
&PaladinAiObjectContextInternal::cast_greater_blessing_assignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -414,15 +422,41 @@ private:
|
|||||||
static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); }
|
static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); }
|
||||||
static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); }
|
static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); }
|
||||||
static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); }
|
static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); }
|
||||||
static Action* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); }
|
static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); }
|
||||||
static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); }
|
static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); }
|
||||||
static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); }
|
static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); }
|
||||||
static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); }
|
static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); }
|
||||||
static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); }
|
static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); }
|
||||||
static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); }
|
static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); }
|
||||||
static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); }
|
static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); }
|
||||||
static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); }
|
static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); }
|
||||||
static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); }
|
static Action* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new CastHandOfFreedomOnPartyAction(botAI); }
|
||||||
|
static Action* cast_greater_blessing_assignment(PlayerbotAI* botAI)
|
||||||
|
{
|
||||||
|
return new CastGreaterBlessingAssignmentAction(botAI);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class PaladinValueContextInternal : public NamedObjectContext<UntypedValue>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PaladinValueContextInternal()
|
||||||
|
{
|
||||||
|
creators["greater blessing assignments"] = &PaladinValueContextInternal::greater_blessing_assignments;
|
||||||
|
creators["greater blessing pending assignment"] =
|
||||||
|
&PaladinValueContextInternal::greater_blessing_pending_assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static UntypedValue* greater_blessing_assignments(PlayerbotAI* botAI)
|
||||||
|
{
|
||||||
|
return ai::gbless::greater_blessing_assignments_value(botAI);
|
||||||
|
}
|
||||||
|
|
||||||
|
static UntypedValue* greater_blessing_pending_assignment(PlayerbotAI* botAI)
|
||||||
|
{
|
||||||
|
return ai::gbless::greater_blessing_pending_assignment_value(botAI);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SharedNamedObjectContextList<Strategy> PaladinAiObjectContext::sharedStrategyContexts;
|
SharedNamedObjectContextList<Strategy> PaladinAiObjectContext::sharedStrategyContexts;
|
||||||
@ -467,4 +501,5 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext
|
|||||||
void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts)
|
void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts)
|
||||||
{
|
{
|
||||||
AiObjectContext::BuildSharedValueContexts(valueContexts);
|
AiObjectContext::BuildSharedValueContexts(valueContexts);
|
||||||
|
valueContexts.Add(new PaladinValueContextInternal());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,4 +30,7 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
|
|||||||
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
|
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
|
||||||
if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION)
|
if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION)
|
||||||
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
|
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
|
||||||
|
|
||||||
|
triggers.push_back(new TriggerNode("greater blessing needed",
|
||||||
|
{ NextAction("cast greater blessing assignment", ACTION_NORMAL) }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public:
|
|||||||
PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
||||||
std::string const getName() override { return "bmana"; }
|
std::string const getName() override { return "bwisdom"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinBuffHealthStrategy : public Strategy
|
class PaladinBuffHealthStrategy : public Strategy
|
||||||
@ -25,7 +25,7 @@ public:
|
|||||||
PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
||||||
std::string const getName() override { return "bhealth"; }
|
std::string const getName() override { return "bsanc"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinBuffDpsStrategy : public Strategy
|
class PaladinBuffDpsStrategy : public Strategy
|
||||||
@ -34,7 +34,7 @@ public:
|
|||||||
PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
||||||
std::string const getName() override { return "bdps"; }
|
std::string const getName() override { return "bmight"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinBuffArmorStrategy : public Strategy
|
class PaladinBuffArmorStrategy : public Strategy
|
||||||
@ -88,7 +88,7 @@ public:
|
|||||||
PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
|
||||||
std::string const getName() override { return "bstats"; }
|
std::string const getName() override { return "bkings"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PaladinShadowResistanceStrategy : public Strategy
|
class PaladinShadowResistanceStrategy : public Strategy
|
||||||
|
|||||||
@ -95,7 +95,8 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
triggers.push_back(new TriggerNode(
|
triggers.push_back(
|
||||||
|
new TriggerNode(
|
||||||
"light aoe",
|
"light aoe",
|
||||||
{
|
{
|
||||||
NextAction("avenger's shield", ACTION_HIGH + 5)
|
NextAction("avenger's shield", ACTION_HIGH + 5)
|
||||||
@ -122,21 +123,6 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
triggers.push_back(
|
triggers.push_back(
|
||||||
new TriggerNode(
|
new TriggerNode(
|
||||||
"medium health",
|
"medium health",
|
||||||
{ NextAction("holy shield", ACTION_HIGH + 4)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
triggers.push_back(
|
|
||||||
new TriggerNode(
|
|
||||||
"low health",
|
|
||||||
{
|
|
||||||
NextAction("holy shield", ACTION_HIGH + 4)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
triggers.push_back(
|
|
||||||
new TriggerNode(
|
|
||||||
"critical health",
|
|
||||||
{
|
{
|
||||||
NextAction("holy shield", ACTION_HIGH + 4)
|
NextAction("holy shield", ACTION_HIGH + 4)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,11 @@
|
|||||||
|
|
||||||
#include "PaladinTriggers.h"
|
#include "PaladinTriggers.h"
|
||||||
|
|
||||||
|
#include "GenericBuffUtils.h"
|
||||||
|
#include "PaladinGreaterBlessingAction.h"
|
||||||
#include "PaladinActions.h"
|
#include "PaladinActions.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
|
||||||
#include "Playerbots.h"
|
|
||||||
#include "PaladinHelper.h"
|
#include "PaladinHelper.h"
|
||||||
|
#include "Playerbots.h"
|
||||||
|
|
||||||
bool SealTrigger::IsActive()
|
bool SealTrigger::IsActive()
|
||||||
{
|
{
|
||||||
@ -28,7 +29,8 @@ bool CrusaderAuraTrigger::IsActive()
|
|||||||
bool BlessingTrigger::IsActive()
|
bool BlessingTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
|
return SpellTrigger::IsActive() &&
|
||||||
|
!botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
|
||||||
"blessing of kings", "blessing of sanctuary", nullptr);
|
"blessing of kings", "blessing of sanctuary", nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +64,8 @@ bool HandOfFreedomOnPartyTrigger::IsActive()
|
|||||||
if (!target)
|
if (!target)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f)
|
if (target != bot &&
|
||||||
|
bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!botAI->CanCastSpell("hand of freedom", target))
|
if (!botAI->CanCastSpell("hand of freedom", target))
|
||||||
@ -75,3 +78,29 @@ bool NotSensingUndeadTrigger::IsActive()
|
|||||||
{
|
{
|
||||||
return !botAI->HasAura("sense undead", bot);
|
return !botAI->HasAura("sense undead", bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GreaterBlessingNeededTrigger::IsActive()
|
||||||
|
{
|
||||||
|
if (!ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ai::buff::ShouldDeferGreaterBlessingAssignmentForRecentLogin(bot))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Group* group = bot->GetGroup();
|
||||||
|
uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0;
|
||||||
|
|
||||||
|
Value<ai::gbless::CachedPendingBlessingAssignment>* pendingValue =
|
||||||
|
context->GetValue<ai::gbless::CachedPendingBlessingAssignment>("greater blessing pending assignment");
|
||||||
|
if (!pendingValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get();
|
||||||
|
if (pendingAssignment.groupKey != groupKey)
|
||||||
|
{
|
||||||
|
pendingValue->Reset();
|
||||||
|
pendingAssignment = pendingValue->Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingAssignment.valid && pendingAssignment.groupKey == groupKey;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,32 +13,6 @@
|
|||||||
|
|
||||||
class PlayerbotAI;
|
class PlayerbotAI;
|
||||||
|
|
||||||
inline std::string const GetActualBlessingOfMight(Unit* target)
|
|
||||||
{
|
|
||||||
switch (target->getClass())
|
|
||||||
{
|
|
||||||
case CLASS_MAGE:
|
|
||||||
case CLASS_PRIEST:
|
|
||||||
case CLASS_WARLOCK:
|
|
||||||
return "blessing of wisdom";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "blessing of might";
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string const GetActualBlessingOfWisdom(Unit* target)
|
|
||||||
{
|
|
||||||
switch (target->getClass())
|
|
||||||
{
|
|
||||||
case CLASS_WARRIOR:
|
|
||||||
case CLASS_ROGUE:
|
|
||||||
case CLASS_DEATH_KNIGHT:
|
|
||||||
return "blessing of might";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "blessing of wisdom";
|
|
||||||
}
|
|
||||||
|
|
||||||
BUFF_TRIGGER(HolyShieldTrigger, "holy shield");
|
BUFF_TRIGGER(HolyShieldTrigger, "holy shield");
|
||||||
BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury");
|
BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury");
|
||||||
|
|
||||||
@ -212,42 +186,55 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
|
|||||||
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
|
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
|
BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
|
: BuffOnMainTankTrigger(botAI, "beacon of light", true) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
|
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SacredShieldOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "sacred shield", false) {}
|
SacredShieldOnMainTankTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffOnMainTankTrigger(botAI, "sacred shield", false) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger
|
class BlessingOfKingsOnPartyTrigger : public BlessingOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
|
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {}
|
: BlessingOnPartyTrigger(botAI)
|
||||||
|
{
|
||||||
|
spell = "blessing of kings";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger
|
class BlessingOfWisdomOnPartyTrigger : public BlessingOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
|
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
|
: BlessingOnPartyTrigger(botAI)
|
||||||
|
{
|
||||||
|
spell = "blessing of might,blessing of wisdom";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger
|
class BlessingOfMightOnPartyTrigger : public BlessingOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
|
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
|
: BlessingOnPartyTrigger(botAI)
|
||||||
|
{
|
||||||
|
spell = "blessing of might,blessing of wisdom";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger
|
class BlessingOfSanctuaryOnPartyTrigger : public BlessingOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
|
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
|
: BlessingOnPartyTrigger(botAI)
|
||||||
|
{
|
||||||
|
spell = "blessing of sanctuary";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class HandOfFreedomOnPartyTrigger : public Trigger
|
class HandOfFreedomOnPartyTrigger : public Trigger
|
||||||
@ -266,4 +253,13 @@ public:
|
|||||||
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
|
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class GreaterBlessingNeededTrigger : public Trigger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GreaterBlessingNeededTrigger(PlayerbotAI* botAI)
|
||||||
|
: Trigger(botAI, "greater blessing needed", 4) {}
|
||||||
|
|
||||||
|
bool IsActive() override;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -18,6 +18,8 @@ static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022;
|
|||||||
static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038;
|
static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038;
|
||||||
static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044;
|
static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044;
|
||||||
static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940;
|
static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940;
|
||||||
|
static constexpr uint32 SPELL_BLESSING_OF_SANCTUARY = 20911;
|
||||||
|
static constexpr uint32 SPELL_GREATER_BLESSING_OF_SANCTUARY = 25899;
|
||||||
|
|
||||||
inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list<uint32> spellIds)
|
inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list<uint32> spellIds)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,9 +13,19 @@
|
|||||||
class PlayerbotAI;
|
class PlayerbotAI;
|
||||||
|
|
||||||
// disc
|
// disc
|
||||||
BUFF_ACTION(CastPowerWordFortitudeAction, "power word: fortitude");
|
class CastPowerWordFortitudeAction : public GroupBuffSpellAction
|
||||||
BUFF_PARTY_ACTION(CastPowerWordFortitudeOnPartyAction, "power word: fortitude");
|
{
|
||||||
BUFF_PARTY_ACTION(CastPrayerOfFortitudeOnPartyAction, "prayer of fortitude");
|
public:
|
||||||
|
CastPowerWordFortitudeAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffSpellAction(botAI, "power word: fortitude") {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CastPowerWordFortitudeOnPartyAction : public GroupBuffOnPartyAction
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CastPowerWordFortitudeOnPartyAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffOnPartyAction(botAI, "power word: fortitude") {}
|
||||||
|
};
|
||||||
BUFF_ACTION(CastPowerWordShieldAction, "power word: shield");
|
BUFF_ACTION(CastPowerWordShieldAction, "power word: shield");
|
||||||
|
|
||||||
BUFF_ACTION(CastInnerFireAction, "inner fire");
|
BUFF_ACTION(CastInnerFireAction, "inner fire");
|
||||||
@ -26,9 +36,19 @@ CC_ACTION(CastShackleUndeadAction, "shackle undead");
|
|||||||
SPELL_ACTION_U(CastManaBurnAction, "mana burn",
|
SPELL_ACTION_U(CastManaBurnAction, "mana burn",
|
||||||
AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20);
|
AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20);
|
||||||
BUFF_ACTION(CastLevitateAction, "levitate");
|
BUFF_ACTION(CastLevitateAction, "levitate");
|
||||||
BUFF_ACTION(CastDivineSpiritAction, "divine spirit");
|
class CastDivineSpiritAction : public GroupBuffSpellAction
|
||||||
BUFF_PARTY_ACTION(CastDivineSpiritOnPartyAction, "divine spirit");
|
{
|
||||||
BUFF_PARTY_ACTION(CastPrayerOfSpiritOnPartyAction, "prayer of spirit");
|
public:
|
||||||
|
CastDivineSpiritAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffSpellAction(botAI, "divine spirit") {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CastDivineSpiritOnPartyAction : public GroupBuffOnPartyAction
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CastDivineSpiritOnPartyAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffOnPartyAction(botAI, "divine spirit") {}
|
||||||
|
};
|
||||||
// disc 2.4.3
|
// disc 2.4.3
|
||||||
SPELL_ACTION(CastMassDispelAction, "mass dispel");
|
SPELL_ACTION(CastMassDispelAction, "mass dispel");
|
||||||
|
|
||||||
@ -103,13 +123,23 @@ SPELL_ACTION(CastMindBlastAction, "mind blast");
|
|||||||
SPELL_ACTION(CastPsychicScreamAction, "psychic scream");
|
SPELL_ACTION(CastPsychicScreamAction, "psychic scream");
|
||||||
DEBUFF_ACTION(CastMindSootheAction, "mind soothe");
|
DEBUFF_ACTION(CastMindSootheAction, "mind soothe");
|
||||||
BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup());
|
BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup());
|
||||||
BUFF_ACTION(CastShadowProtectionAction, "shadow protection");
|
class CastShadowProtectionAction : public GroupBuffSpellAction
|
||||||
BUFF_PARTY_ACTION(CastShadowProtectionOnPartyAction, "shadow protection");
|
{
|
||||||
BUFF_PARTY_ACTION(CastPrayerOfShadowProtectionAction, "prayer of shadow protection");
|
public:
|
||||||
|
CastShadowProtectionAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffSpellAction(botAI, "shadow protection") {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CastShadowProtectionOnPartyAction : public GroupBuffOnPartyAction
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CastShadowProtectionOnPartyAction(PlayerbotAI* botAI)
|
||||||
|
: GroupBuffOnPartyAction(botAI, "shadow protection") {}
|
||||||
|
};
|
||||||
|
|
||||||
// shadow talents
|
// shadow talents
|
||||||
SPELL_ACTION(CastMindFlayAction, "mind flay");
|
SPELL_ACTION(CastMindFlayAction, "mind flay");
|
||||||
DEBUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace");
|
BUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace");
|
||||||
BUFF_ACTION(CastShadowformAction, "shadowform");
|
BUFF_ACTION(CastShadowformAction, "shadowform");
|
||||||
SPELL_ACTION(CastSilenceAction, "silence");
|
SPELL_ACTION(CastSilenceAction, "silence");
|
||||||
ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence");
|
ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence");
|
||||||
|
|||||||
@ -92,8 +92,6 @@ public:
|
|||||||
creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection;
|
creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection;
|
||||||
creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party;
|
creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party;
|
||||||
creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead;
|
creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead;
|
||||||
creators["prayer of fortitude on party"] = &PriestTriggerFactoryInternal::prayer_of_fortitude_on_party;
|
|
||||||
creators["prayer of spirit on party"] = &PriestTriggerFactoryInternal::prayer_of_spirit_on_party;
|
|
||||||
creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire;
|
creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire;
|
||||||
creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness;
|
creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness;
|
||||||
creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness;
|
creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness;
|
||||||
@ -136,8 +134,6 @@ private:
|
|||||||
static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); }
|
static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); }
|
||||||
static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); }
|
static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); }
|
||||||
static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); }
|
static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); }
|
||||||
static Trigger* prayer_of_fortitude_on_party(PlayerbotAI* botAI) { return new PrayerOfFortitudeTrigger(botAI); }
|
|
||||||
static Trigger* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new PrayerOfSpiritTrigger(botAI); }
|
|
||||||
static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); }
|
static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); }
|
||||||
static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); }
|
static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); }
|
||||||
static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); }
|
static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); }
|
||||||
@ -207,8 +203,6 @@ public:
|
|||||||
creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection;
|
creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection;
|
||||||
creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party;
|
creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party;
|
||||||
creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead;
|
creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead;
|
||||||
creators["prayer of fortitude on party"] = &PriestAiObjectContextInternal::prayer_of_fortitude_on_party;
|
|
||||||
creators["prayer of spirit on party"] = &PriestAiObjectContextInternal::prayer_of_spirit_on_party;
|
|
||||||
creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party;
|
creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party;
|
||||||
creators["silence"] = &PriestAiObjectContextInternal::silence;
|
creators["silence"] = &PriestAiObjectContextInternal::silence;
|
||||||
creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer;
|
creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer;
|
||||||
@ -311,11 +305,6 @@ private:
|
|||||||
static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); }
|
static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); }
|
||||||
static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); }
|
static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); }
|
||||||
static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); }
|
static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); }
|
||||||
static Action* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new CastPrayerOfSpiritOnPartyAction(botAI); }
|
|
||||||
static Action* prayer_of_fortitude_on_party(PlayerbotAI* botAI)
|
|
||||||
{
|
|
||||||
return new CastPrayerOfFortitudeOnPartyAction(botAI);
|
|
||||||
}
|
|
||||||
static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); }
|
static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); }
|
||||||
static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); }
|
static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); }
|
||||||
static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); }
|
static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); }
|
||||||
|
|||||||
@ -19,6 +19,8 @@ void PriestNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
|
|
||||||
triggers.push_back(
|
triggers.push_back(
|
||||||
new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) }));
|
new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) }));
|
||||||
|
triggers.push_back(
|
||||||
|
new TriggerNode("vampiric embrace", { NextAction("vampiric embrace", 16.0f) }));
|
||||||
triggers.push_back(new TriggerNode(
|
triggers.push_back(new TriggerNode(
|
||||||
"party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11),
|
"party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11),
|
||||||
NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) }));
|
NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) }));
|
||||||
@ -54,12 +56,6 @@ void PriestBuffStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
{
|
{
|
||||||
NonCombatStrategy::InitTriggers(triggers);
|
NonCombatStrategy::InitTriggers(triggers);
|
||||||
|
|
||||||
triggers.push_back(
|
|
||||||
new TriggerNode("prayer of fortitude on party",
|
|
||||||
{ NextAction("prayer of fortitude on party", 12.0f) }));
|
|
||||||
triggers.push_back(
|
|
||||||
new TriggerNode("prayer of spirit on party",
|
|
||||||
{ NextAction("prayer of spirit on party", 14.0f) }));
|
|
||||||
triggers.push_back(
|
triggers.push_back(
|
||||||
new TriggerNode("power word: fortitude on party",
|
new TriggerNode("power word: fortitude on party",
|
||||||
{ NextAction("power word: fortitude on party", 11.0f) }));
|
{ NextAction("power word: fortitude on party", 11.0f) }));
|
||||||
|
|||||||
@ -30,8 +30,6 @@ public:
|
|||||||
creators["flash heal"] = &flash_heal;
|
creators["flash heal"] = &flash_heal;
|
||||||
creators["flash heal on party"] = &flash_heal_on_party;
|
creators["flash heal on party"] = &flash_heal_on_party;
|
||||||
creators["circle of healing on party"] = &circle_of_healing;
|
creators["circle of healing on party"] = &circle_of_healing;
|
||||||
creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party;
|
|
||||||
creators["prayer of spirit on party"] = &prayer_of_spirit_on_party;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -134,20 +132,6 @@ private:
|
|||||||
/*A*/ {},
|
/*A*/ {},
|
||||||
/*C*/ {});
|
/*C*/ {});
|
||||||
}
|
}
|
||||||
static ActionNode* prayer_of_fortitude_on_party([[maybe_unused]] PlayerbotAI* botAI)
|
|
||||||
{
|
|
||||||
return new ActionNode("prayer of fortitude on party",
|
|
||||||
/*P*/ { NextAction("remove shadowform") },
|
|
||||||
/*A*/ { NextAction("power word: fortitude on party") },
|
|
||||||
/*C*/ {});
|
|
||||||
}
|
|
||||||
static ActionNode* prayer_of_spirit_on_party([[maybe_unused]] PlayerbotAI* botAI)
|
|
||||||
{
|
|
||||||
return new ActionNode("prayer of spirit on party",
|
|
||||||
/*P*/ { NextAction("remove shadowform") },
|
|
||||||
/*A*/ { NextAction("divine spirit on party") },
|
|
||||||
/*C*/ {});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -51,14 +51,6 @@ void ShadowPriestStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
triggers.push_back(
|
|
||||||
new TriggerNode(
|
|
||||||
"vampiric embrace",
|
|
||||||
{
|
|
||||||
NextAction("vampiric embrace", 16.0f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
triggers.push_back(
|
triggers.push_back(
|
||||||
new TriggerNode(
|
new TriggerNode(
|
||||||
"silence",
|
"silence",
|
||||||
|
|||||||
@ -8,10 +8,9 @@
|
|||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
|
|
||||||
bool PowerWordFortitudeOnPartyTrigger::IsActive()
|
bool ShadowProtectionTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) &&
|
return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget());
|
||||||
!botAI->HasAura("prayer of fortitude", GetTarget());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PowerWordFortitudeTrigger::IsActive()
|
bool PowerWordFortitudeTrigger::IsActive()
|
||||||
@ -20,43 +19,12 @@ bool PowerWordFortitudeTrigger::IsActive()
|
|||||||
!botAI->HasAura("prayer of fortitude", GetTarget());
|
!botAI->HasAura("prayer of fortitude", GetTarget());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DivineSpiritOnPartyTrigger::IsActive()
|
|
||||||
{
|
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
|
|
||||||
!botAI->HasAura("prayer of spirit", GetTarget());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DivineSpiritTrigger::IsActive()
|
bool DivineSpiritTrigger::IsActive()
|
||||||
{
|
{
|
||||||
return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
|
return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
|
||||||
!botAI->HasAura("prayer of spirit", GetTarget());
|
!botAI->HasAura("prayer of spirit", GetTarget());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PrayerOfFortitudeTrigger::IsActive()
|
|
||||||
{
|
|
||||||
Unit* target = GetTarget();
|
|
||||||
if (!target || !target->IsPlayer())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of fortitude", GetTarget()) &&
|
|
||||||
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
|
|
||||||
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of fortitude") < 4 &&
|
|
||||||
!botAI->GetBuffedCount((Player*)GetTarget(), "power word: fortitude");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrayerOfSpiritTrigger::IsActive()
|
|
||||||
{
|
|
||||||
Unit* target = GetTarget();
|
|
||||||
if (!target || !target->IsPlayer())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of spirit", GetTarget()) &&
|
|
||||||
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
|
|
||||||
// botAI->GetManaPercent() > 50 &&
|
|
||||||
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of spirit") < 4 &&
|
|
||||||
!botAI->GetBuffedCount((Player*)GetTarget(), "divine spirit");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool InnerFireTrigger::IsActive()
|
bool InnerFireTrigger::IsActive()
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
|
|||||||
@ -27,8 +27,6 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire");
|
|||||||
BUFF_TRIGGER_A(ShadowformTrigger, "shadowform");
|
BUFF_TRIGGER_A(ShadowformTrigger, "shadowform");
|
||||||
BOOST_TRIGGER(PowerInfusionTrigger, "power infusion");
|
BOOST_TRIGGER(PowerInfusionTrigger, "power infusion");
|
||||||
BUFF_TRIGGER(InnerFocusTrigger, "inner focus");
|
BUFF_TRIGGER(InnerFocusTrigger, "inner focus");
|
||||||
BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection");
|
|
||||||
BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection");
|
|
||||||
CC_TRIGGER(ShackleUndeadTrigger, "shackle undead");
|
CC_TRIGGER(ShackleUndeadTrigger, "shackle undead");
|
||||||
INTERRUPT_TRIGGER(SilenceTrigger, "silence");
|
INTERRUPT_TRIGGER(SilenceTrigger, "silence");
|
||||||
INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence");
|
INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence");
|
||||||
@ -44,20 +42,34 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise");
|
|||||||
|
|
||||||
BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend");
|
BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend");
|
||||||
|
|
||||||
|
class ShadowProtectionTrigger : public BuffTrigger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ShadowProtectionTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffTrigger(botAI, "shadow protection", 4 * 2000) {}
|
||||||
|
|
||||||
|
bool IsActive() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ShadowProtectionOnPartyTrigger : public BuffOnPartyTrigger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ShadowProtectionOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffOnPartyTrigger(botAI, "shadow protection", 4 * 2000) {}
|
||||||
|
};
|
||||||
|
|
||||||
class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger
|
class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000)
|
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
{
|
: BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {}
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PowerWordFortitudeTrigger : public BuffTrigger
|
class PowerWordFortitudeTrigger : public BuffTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
|
PowerWordFortitudeTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -65,31 +77,15 @@ public:
|
|||||||
class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger
|
class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
|
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
|
||||||
bool IsActive() override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DivineSpiritTrigger : public BuffTrigger
|
class DivineSpiritTrigger : public BuffTrigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DivineSpiritTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
|
DivineSpiritTrigger(PlayerbotAI* botAI)
|
||||||
|
: BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
|
||||||
bool IsActive() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PrayerOfFortitudeTrigger : public BuffOnPartyTrigger
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PrayerOfFortitudeTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of fortitude", 3 * 2000) {}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PrayerOfSpiritTrigger : public BuffOnPartyTrigger
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PrayerOfSpiritTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of spirit", 2 * 2000) {}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
@ -106,9 +102,7 @@ class MindSearChannelCheckTrigger : public Trigger
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2)
|
MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2)
|
||||||
: Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies)
|
: Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
|
|
||||||
|
|||||||
@ -1462,15 +1462,6 @@ bool HodirBitingColdJumpAction::Execute(Event /*event*/)
|
|||||||
// float speed = 7.96f;
|
// float speed = 7.96f;
|
||||||
|
|
||||||
// UpdateMovementState();
|
// UpdateMovementState();
|
||||||
// if (!IsMovingAllowed(mapId, x, y, z))
|
|
||||||
//{
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// MovementPriority priority;
|
|
||||||
// if (IsWaitingForLastMove(priority))
|
|
||||||
//{
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// MotionMaster& mm = *bot->GetMotionMaster();
|
// MotionMaster& mm = *bot->GetMotionMaster();
|
||||||
// mm.Clear();
|
// mm.Clear();
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
#include "PathGenerator.h"
|
#include "PathGenerator.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "PlayerbotAI.h"
|
#include "PlayerbotAI.h"
|
||||||
|
#include "Playerbots.h"
|
||||||
#include "QuestDef.h"
|
#include "QuestDef.h"
|
||||||
#include "Random.h"
|
#include "Random.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
@ -120,17 +121,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case RPG_TRAVEL_FLIGHT:
|
// RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction
|
||||||
{
|
// so the flight action owns both take-off and landing transitions.
|
||||||
auto& data = std::get<NewRpgInfo::TravelFlight>(info.data);
|
|
||||||
if (data.inFlight && !bot->IsInFlight())
|
|
||||||
{
|
|
||||||
// flight arrival
|
|
||||||
info.ChangeToIdle();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RPG_REST:
|
case RPG_REST:
|
||||||
{
|
{
|
||||||
// REST -> IDLE
|
// REST -> IDLE
|
||||||
@ -301,6 +293,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
data.pos = WorldPosition();
|
||||||
data.objectiveIdx = 0;
|
data.objectiveIdx = 0;
|
||||||
|
data.pursuedLootGO.Clear();
|
||||||
|
data.pursuedUseGO.Clear();
|
||||||
|
data.pursuedUseTarget.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.pos == WorldPosition())
|
if (data.pos == WorldPosition())
|
||||||
@ -329,15 +324,34 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = pos;
|
data.pos = pos;
|
||||||
data.objectiveIdx = objectiveIdx;
|
data.objectiveIdx = objectiveIdx;
|
||||||
|
data.pursuedLootGO.Clear();
|
||||||
|
data.pursuedUseGO.Clear();
|
||||||
|
data.pursuedUseTarget.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
|
||||||
{
|
{
|
||||||
|
// Yield to attack-anything ONLY if a mob needed by this exact
|
||||||
|
// quest+objective is right next to us. The broad variant (any
|
||||||
|
// quest in the log) yielded for every nearby mob and derailed
|
||||||
|
// turn-ins / cross-zone travel through other quests' clusters.
|
||||||
|
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Note: previously yielded ~10%/tick when any hostile was
|
||||||
|
// within 25y. That overrode the do-quest multiplier in
|
||||||
|
// practice (combined with bots getting aggroed on the way,
|
||||||
|
// which ALSO bypasses the multiplier via combat engine) and
|
||||||
|
// bots ended up grinding their way to POIs instead of
|
||||||
|
// travelling. Quest-mob exception above is kept so we don't
|
||||||
|
// walk past a quest target while gathering. Anything else
|
||||||
|
// hostile is the multiplier's job to throttle — and bots
|
||||||
|
// that DO get aggroed switch to combat engine where the
|
||||||
|
// class strategy handles it.
|
||||||
|
|
||||||
if (MoveFarTo(data.pos))
|
if (MoveFarTo(data.pos))
|
||||||
return true;
|
return true;
|
||||||
// Long-range sampler couldn't land a candidate — nudge the
|
// sampler found nothing — nudge so next tick tries a new pos
|
||||||
// bot a short distance so the next tick retries from a
|
|
||||||
// different position instead of sitting idle.
|
|
||||||
return MoveRandomNear(10.0f);
|
return MoveRandomNear(10.0f);
|
||||||
}
|
}
|
||||||
// Now we are near the quest objective
|
// Now we are near the quest objective
|
||||||
@ -382,14 +396,111 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = WorldPosition();
|
data.pos = WorldPosition();
|
||||||
data.objectiveIdx = 0;
|
data.objectiveIdx = 0;
|
||||||
|
data.pursuedLootGO.Clear();
|
||||||
|
data.pursuedUseGO.Clear();
|
||||||
|
data.pursuedUseTarget.Clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// At the POI: keep the bot actively placed but avoid large
|
// at POI: drive toward specific objectives first
|
||||||
// random 20yd hops that look like pacing back and forth. A small
|
if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget))
|
||||||
// ~8yd wander reads as the bot looking around while grind/loot
|
return true;
|
||||||
// strategies do their work.
|
if (TryLootQuestGO(data.pursuedLootGO))
|
||||||
return MoveRandomNear(8.0f);
|
return true;
|
||||||
|
if (TryUseQuestGO(data.pursuedUseGO))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// gather quests: roam for spawns. kill quests: yield to grind.
|
||||||
|
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
|
||||||
|
if (quest)
|
||||||
|
{
|
||||||
|
int32 obj = data.objectiveIdx;
|
||||||
|
bool isGatherObjective = false;
|
||||||
|
if (obj < QUEST_OBJECTIVES_COUNT)
|
||||||
|
{
|
||||||
|
int32 entry = quest->RequiredNpcOrGo[obj];
|
||||||
|
if (entry < 0) // GO objective
|
||||||
|
isGatherObjective = true;
|
||||||
|
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && quest->RequiredItemId[obj])
|
||||||
|
isGatherObjective = true;
|
||||||
|
}
|
||||||
|
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
|
||||||
|
{
|
||||||
|
isGatherObjective = true;
|
||||||
|
}
|
||||||
|
// source-item quest: need to find the target to use it on
|
||||||
|
if (quest->GetSrcItemId())
|
||||||
|
isGatherObjective = true;
|
||||||
|
|
||||||
|
if (isGatherObjective)
|
||||||
|
return MoveRandomNear(20.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill-quest scout: at POI for 30s+ with no quest mob in sight
|
||||||
|
// means this cluster is empty. Switch to a different POI candidate
|
||||||
|
// (>50y away) if one exists; otherwise roam in place.
|
||||||
|
constexpr uint32 scoutTimeoutMs = 30 * 1000;
|
||||||
|
if (data.lastReachPOI && GetMSTimeDiffToNow(data.lastReachPOI) >= scoutTimeoutMs &&
|
||||||
|
!HasNearbyQuestMob(30.0f))
|
||||||
|
{
|
||||||
|
std::vector<POIInfo> poiInfo;
|
||||||
|
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
|
||||||
|
{
|
||||||
|
std::vector<size_t> alternatives;
|
||||||
|
for (size_t i = 0; i < poiInfo.size(); ++i)
|
||||||
|
{
|
||||||
|
float dx = poiInfo[i].pos.x - data.pos.GetPositionX();
|
||||||
|
float dy = poiInfo[i].pos.y - data.pos.GetPositionY();
|
||||||
|
if (dx * dx + dy * dy > 50.0f * 50.0f)
|
||||||
|
alternatives.push_back(i);
|
||||||
|
}
|
||||||
|
if (!alternatives.empty())
|
||||||
|
{
|
||||||
|
size_t pickIdx = alternatives[urand(0, alternatives.size() - 1)];
|
||||||
|
G3D::Vector2 newPoi = poiInfo[pickIdx].pos;
|
||||||
|
float dz = std::max(bot->GetMap()->GetHeight(newPoi.x, newPoi.y, MAX_HEIGHT),
|
||||||
|
bot->GetMap()->GetWaterLevel(newPoi.x, newPoi.y));
|
||||||
|
if (dz != INVALID_HEIGHT && dz != VMAP_INVALID_HEIGHT_VALUE)
|
||||||
|
{
|
||||||
|
data.pos = WorldPosition(bot->GetMapId(), newPoi.x, newPoi.y, dz);
|
||||||
|
data.objectiveIdx = poiInfo[pickIdx].objectiveIdx;
|
||||||
|
data.lastReachPOI = 0;
|
||||||
|
data.pursuedLootGO.Clear();
|
||||||
|
data.pursuedUseGO.Clear();
|
||||||
|
data.pursuedUseTarget.Clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MoveRandomNear(20.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// kill quest: walk toward the marker before handing off to grind.
|
||||||
|
// lastReachPOI trips at ~10y so without this the bot fights on the
|
||||||
|
// edge and never reaches the dense cluster. Skip if a quest mob is
|
||||||
|
// in sight (might be the target) or a hostile is mid-pull.
|
||||||
|
if (bot->GetDistance(data.pos) > 5.0f)
|
||||||
|
{
|
||||||
|
if (HasNearbyQuestMob(30.0f))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
GuidVector nearby = AI_VALUE(GuidVector, "possible targets");
|
||||||
|
bool hostileClose = false;
|
||||||
|
for (ObjectGuid guid : nearby)
|
||||||
|
{
|
||||||
|
Unit* u = botAI->GetUnit(guid);
|
||||||
|
if (u && u->IsAlive() && bot->GetDistance(u) < 15.0f)
|
||||||
|
{
|
||||||
|
hostileClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hostileClose)
|
||||||
|
return MoveFarTo(data.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// yield to grind
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
||||||
@ -423,6 +534,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
|||||||
data.lastReachPOI = 0;
|
data.lastReachPOI = 0;
|
||||||
data.pos = pos;
|
data.pos = pos;
|
||||||
data.objectiveIdx = -1;
|
data.objectiveIdx = -1;
|
||||||
|
|
||||||
|
// Drop the spline + lastPath that DoIncompleteQuest committed
|
||||||
|
// to the now-completed objective. Without this, MoveFarTo on
|
||||||
|
// the next tick hits the bot->isMoving() / lastPath-reuse
|
||||||
|
// early-exits at the top of MoveFarTo and rides the stale
|
||||||
|
// path instead of replanning toward the turn-in POI. (This
|
||||||
|
// is what `.playerbot bot self` masks by recreating the AI.)
|
||||||
|
bot->GetMotionMaster()->Clear();
|
||||||
|
AI_VALUE(LastMovement&, "last movement").clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.pos == WorldPosition())
|
if (data.pos == WorldPosition())
|
||||||
@ -453,7 +573,9 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
|||||||
botAI->rpgInfo.ChangeToIdle();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
// waiting for SearchQuestGiverAndAcceptOrReward to pick up the NPC;
|
||||||
|
// wander instead of false so we don't fall through to grind
|
||||||
|
return MoveRandomNear(15.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
||||||
@ -464,6 +586,22 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto& data = *dataPtr;
|
auto& data = *dataPtr;
|
||||||
|
|
||||||
|
// Arrival: we had boarded a flight (data.inFlight) and we're no longer in
|
||||||
|
// it → we just landed. Special-case Rut'theran: walk to the portal GO so
|
||||||
|
// it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS
|
||||||
|
// so this branch falls through to ChangeToIdle on the next tick.
|
||||||
|
if (data.inFlight && !bot->IsInFlight())
|
||||||
|
{
|
||||||
|
if (bot->GetZoneId() == AREA_TELDRASSIL)
|
||||||
|
{
|
||||||
|
static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f);
|
||||||
|
return MoveFarTo(rutTheranPortalEntrance);
|
||||||
|
}
|
||||||
|
info.ChangeToIdle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (bot->IsInFlight())
|
if (bot->IsInFlight())
|
||||||
{
|
{
|
||||||
data.inFlight = true;
|
data.inFlight = true;
|
||||||
@ -479,19 +617,9 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
|
|||||||
info.ChangeToIdle();
|
info.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
|
|
||||||
return MoveFarTo(flightMaster);
|
|
||||||
|
|
||||||
std::vector<uint32> nodes = data.path;
|
if (!TakeFlight(data.path, flightMaster))
|
||||||
|
|
||||||
botAI->RemoveShapeshift();
|
|
||||||
if (bot->IsMounted())
|
|
||||||
bot->Dismount();
|
|
||||||
|
|
||||||
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
|
|
||||||
{
|
{
|
||||||
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
|
|
||||||
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
|
|
||||||
info.ChangeToIdle();
|
info.ChangeToIdle();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ protected:
|
|||||||
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
|
||||||
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr);
|
||||||
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
|
||||||
|
bool TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster);
|
||||||
|
|
||||||
/* QUEST RELATED CHECK */
|
/* QUEST RELATED CHECK */
|
||||||
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
|
||||||
@ -50,6 +51,29 @@ protected:
|
|||||||
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
|
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
|
||||||
bool OrganizeQuestLog();
|
bool OrganizeQuestLog();
|
||||||
|
|
||||||
|
/* QUEST PROGRESSION HELPERS (at POI) */
|
||||||
|
// Walk to a GO that drops a needed quest item. The loot strategy
|
||||||
|
// opens and loots it once in range.
|
||||||
|
bool TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
|
||||||
|
|
||||||
|
// Walk to / use a GO that is itself the objective (rune, lever,
|
||||||
|
// altar, coffin — RequiredNpcOrGo with a negative entry).
|
||||||
|
bool TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange = 60.0f);
|
||||||
|
|
||||||
|
// Fire a quest item's OnUse spell at the right target: a spell-focus
|
||||||
|
// GO (moonwell), a required creature, or the bot itself.
|
||||||
|
bool TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange = 60.0f);
|
||||||
|
|
||||||
|
// True when a quest-relevant mob is within range — used during
|
||||||
|
// travel so we yield to attack-anything instead of running past.
|
||||||
|
bool HasNearbyQuestMob(float range = 20.0f);
|
||||||
|
|
||||||
|
// Narrower variant: only yields for mobs needed by the SPECIFIC
|
||||||
|
// quest+objective the bot is currently working on. Without this,
|
||||||
|
// do-quest yields for any quest in the log, derailing turn-ins
|
||||||
|
// and cross-zone travel through other quests' mob clusters.
|
||||||
|
bool HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
|
||||||
static WorldPosition SelectRandomGrindPos(Player* bot);
|
static WorldPosition SelectRandomGrindPos(Player* bot);
|
||||||
@ -58,17 +82,18 @@ protected:
|
|||||||
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
|
||||||
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
bool CheckRpgStatusAvailable(NewRpgStatus status);
|
||||||
|
|
||||||
protected:
|
private:
|
||||||
/* FOR MOVE FAR */
|
void StartTravelPlan(WorldPosition dest);
|
||||||
const float pathFinderDis = 70.0f;
|
bool UpdateTravelPlan();
|
||||||
// Time without real progress toward dest before MoveFarTo
|
|
||||||
// falls back to teleport recovery. Kept short enough that a
|
// Centralized dispatch helper. Applies underwater fixup, ClipPath
|
||||||
// bot truly oscillating around an unreachable destination
|
// (truncate at first hostile in attack range with LOS, level+5 cap),
|
||||||
// (mmap returning non-progressing partial paths, or NOPATH +
|
// inactive-bot teleport (with self-bot carve-out), masterWalking
|
||||||
// cone fallback wandering) doesn't spin for 5 minutes before
|
// mode, pre-dispatch state cleanup, then dispatches via
|
||||||
// the teleport fires, but long enough that a genuine long
|
// MoveSplinePath and schedules via WaitForReach formula.
|
||||||
// walk that is slowly making progress never triggers it.
|
bool DispatchPathPoints(WorldPosition const& dest,
|
||||||
const uint32 stuckTime = 90 * 1000;
|
Movement::PointsArray& points,
|
||||||
|
char const* label);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
|
|||||||
botAI->rpgInfo.ChangeToIdle();
|
botAI->rpgInfo.ChangeToIdle();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive())
|
if (!bot->IsOutdoorPvPActive())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uint32 zoneId = bot->GetZoneId();
|
uint32 zoneId = bot->GetZoneId();
|
||||||
@ -113,9 +113,6 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
|
|||||||
|
|
||||||
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
|
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
|
||||||
{
|
{
|
||||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Randomly pause at the current spot before picking a new patrol point
|
// Randomly pause at the current spot before picking a new patrol point
|
||||||
if (urand(0, 2) == 0)
|
if (urand(0, 2) == 0)
|
||||||
return ForceToWait(urand(3000, 6000));
|
return ForceToWait(urand(3000, 6000));
|
||||||
|
|||||||
@ -6,31 +6,31 @@
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoGrind{pos};
|
data = GoGrind{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
void NewRpgInfo::ChangeToGoCamp(WorldPosition pos)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = GoCamp{pos};
|
data = GoCamp{pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderNpc()
|
void NewRpgInfo::ChangeToWanderNpc()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderNpc{};
|
data = WanderNpc{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToWanderRandom()
|
void NewRpgInfo::ChangeToWanderRandom()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = WanderRandom{};
|
data = WanderRandom{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
DoQuest do_quest;
|
DoQuest do_quest;
|
||||||
do_quest.questId = questId;
|
do_quest.questId = questId;
|
||||||
do_quest.quest = quest;
|
do_quest.quest = quest;
|
||||||
@ -39,7 +39,7 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
|
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
TravelFlight flight;
|
TravelFlight flight;
|
||||||
flight.flightMasterEntry = flightMasterEntry;
|
flight.flightMasterEntry = flightMasterEntry;
|
||||||
flight.flightMasterPos = flightMasterPos;
|
flight.flightMasterPos = flightMasterPos;
|
||||||
@ -58,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
|
|||||||
|
|
||||||
void NewRpgInfo::ChangeToRest()
|
void NewRpgInfo::ChangeToRest()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Rest{};
|
data = Rest{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgInfo::ChangeToIdle()
|
void NewRpgInfo::ChangeToIdle()
|
||||||
{
|
{
|
||||||
startT = getMSTime();
|
Reset();
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,14 +77,7 @@ void NewRpgInfo::Reset()
|
|||||||
{
|
{
|
||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
}
|
ClearTravel();
|
||||||
|
|
||||||
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
|
|
||||||
{
|
|
||||||
nearestMoveFarDis = FLT_MAX;
|
|
||||||
stuckTs = 0;
|
|
||||||
stuckAttempts = 0;
|
|
||||||
moveFarPos = pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NewRpgStatus NewRpgInfo::GetStatus()
|
NewRpgStatus NewRpgInfo::GetStatus()
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
||||||
#define _PLAYERBOT_NEWRPGINFO_H
|
#define _PLAYERBOT_NEWRPGINFO_H
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
#include "Define.h"
|
#include "Define.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
@ -8,6 +10,7 @@
|
|||||||
#include "Strategy.h"
|
#include "Strategy.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
|
||||||
|
|
||||||
@ -45,6 +48,11 @@ struct NewRpgInfo
|
|||||||
int32 objectiveIdx{0};
|
int32 objectiveIdx{0};
|
||||||
WorldPosition pos{};
|
WorldPosition pos{};
|
||||||
uint32 lastReachPOI{0};
|
uint32 lastReachPOI{0};
|
||||||
|
// committed target per objective type. stops zig-zagging in
|
||||||
|
// dense spawn clusters when "nearest" would flip each tick.
|
||||||
|
ObjectGuid pursuedLootGO{}; // GOs we loot (lilies, eggs)
|
||||||
|
ObjectGuid pursuedUseGO{}; // GOs we click or focus on
|
||||||
|
ObjectGuid pursuedUseTarget{}; // creature we apply an item to
|
||||||
};
|
};
|
||||||
// RPG_TRAVEL_FLIGHT
|
// RPG_TRAVEL_FLIGHT
|
||||||
struct TravelFlight
|
struct TravelFlight
|
||||||
@ -70,12 +78,10 @@ struct NewRpgInfo
|
|||||||
|
|
||||||
uint32 startT{0}; // start timestamp of the current status
|
uint32 startT{0}; // start timestamp of the current status
|
||||||
|
|
||||||
// MOVE_FAR
|
// Travel Node System
|
||||||
float nearestMoveFarDis{FLT_MAX};
|
TravelPlan travelPlan;
|
||||||
uint32 stuckTs{0};
|
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
|
||||||
uint32 stuckAttempts{0};
|
void ClearTravel() { travelPlan.Reset(); }
|
||||||
WorldPosition moveFarPos;
|
|
||||||
// END MOVE_FAR
|
|
||||||
|
|
||||||
using RpgData = std::variant<
|
using RpgData = std::variant<
|
||||||
Idle,
|
Idle,
|
||||||
@ -103,7 +109,6 @@ struct NewRpgInfo
|
|||||||
void ChangeToIdle();
|
void ChangeToIdle();
|
||||||
bool CanChangeTo(NewRpgStatus status);
|
bool CanChangeTo(NewRpgStatus status);
|
||||||
void Reset();
|
void Reset();
|
||||||
void SetMoveFarTo(WorldPosition pos);
|
|
||||||
std::string ToString();
|
std::string ToString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,66 @@
|
|||||||
|
|
||||||
#include "NewRpgStrategy.h"
|
#include "NewRpgStrategy.h"
|
||||||
|
|
||||||
|
#include "Action.h"
|
||||||
|
#include "NewRpgInfo.h"
|
||||||
|
#include "Player.h"
|
||||||
|
#include "PlayerbotAI.h"
|
||||||
|
|
||||||
|
static bool IsGatherObjectiveForDoQuest(NewRpgInfo::DoQuest const* data)
|
||||||
|
{
|
||||||
|
if (!data || !data->quest)
|
||||||
|
return false;
|
||||||
|
Quest const* q = data->quest;
|
||||||
|
int32 obj = data->objectiveIdx;
|
||||||
|
if (obj < QUEST_OBJECTIVES_COUNT)
|
||||||
|
{
|
||||||
|
int32 entry = q->RequiredNpcOrGo[obj];
|
||||||
|
if (entry < 0) // GO objective
|
||||||
|
return true;
|
||||||
|
if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && q->RequiredItemId[obj])
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// source-item quest: need to find the right target to use it on
|
||||||
|
if (q->GetSrcItemId())
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float NewRpgDoQuestMultiplier::GetValue(Action* action)
|
||||||
|
{
|
||||||
|
if (!action || action->getName() != "attack anything")
|
||||||
|
return 1.0f;
|
||||||
|
|
||||||
|
NewRpgInfo& info = botAI->rpgInfo;
|
||||||
|
if (info.GetStatus() != RPG_DO_QUEST)
|
||||||
|
return 1.0f;
|
||||||
|
|
||||||
|
auto* data = std::get_if<NewRpgInfo::DoQuest>(&info.data);
|
||||||
|
if (!data)
|
||||||
|
return 1.0f;
|
||||||
|
|
||||||
|
// heading back to turn in, don't get sidetracked
|
||||||
|
if (data->questId && bot->GetQuestStatus(data->questId) == QUEST_STATUS_COMPLETE)
|
||||||
|
return 0.15f;
|
||||||
|
|
||||||
|
// at POI: gather stays low so mobs don't pull us off the cluster;
|
||||||
|
// kill runs full so attack-anything drives behavior
|
||||||
|
if (data->lastReachPOI)
|
||||||
|
return IsGatherObjectiveForDoQuest(data) ? 0.30f : 1.0f;
|
||||||
|
|
||||||
|
// traveling
|
||||||
|
return 0.20f;
|
||||||
|
}
|
||||||
|
|
||||||
NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
|
||||||
|
|
||||||
std::vector<NextAction> NewRpgStrategy::getDefaultActions()
|
std::vector<NextAction> NewRpgStrategy::getDefaultActions()
|
||||||
{
|
{
|
||||||
// the releavance should be greater than grind
|
// must outrank grind
|
||||||
return {
|
return {
|
||||||
NextAction("new rpg status update", 11.0f)
|
NextAction("new rpg status update", 11.0f)
|
||||||
};
|
};
|
||||||
@ -53,7 +108,8 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
new TriggerNode(
|
new TriggerNode(
|
||||||
"do quest status",
|
"do quest status",
|
||||||
{
|
{
|
||||||
NextAction("new rpg do quest", 3.0f)
|
// 4.5: above attack-anything (4.0), below loot (5.0+)
|
||||||
|
NextAction("new rpg do quest", 4.5f)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -75,6 +131,7 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>&)
|
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
|
||||||
{
|
{
|
||||||
|
multipliers.push_back(new NewRpgDoQuestMultiplier(botAI));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,13 @@
|
|||||||
|
|
||||||
class PlayerbotAI;
|
class PlayerbotAI;
|
||||||
|
|
||||||
|
class NewRpgDoQuestMultiplier : public Multiplier
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NewRpgDoQuestMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "new rpg do quest") {}
|
||||||
|
float GetValue(Action* action) override;
|
||||||
|
};
|
||||||
|
|
||||||
class NewRpgStrategy : public Strategy
|
class NewRpgStrategy : public Strategy
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@ -500,21 +500,21 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const
|
|||||||
switch (player->getClass())
|
switch (player->getClass())
|
||||||
{
|
{
|
||||||
case CLASS_PRIEST:
|
case CLASS_PRIEST:
|
||||||
nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr);
|
nonCombatEngine->addStrategiesNoInit("dps assist", "cure", "rshadow", nullptr);
|
||||||
break;
|
break;
|
||||||
case CLASS_PALADIN:
|
case CLASS_PALADIN:
|
||||||
if (tab == PALADIN_TAB_PROTECTION)
|
if (tab == PALADIN_TAB_PROTECTION)
|
||||||
{
|
{
|
||||||
nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr);
|
nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr);
|
||||||
if (player->GetLevel() >= 20)
|
if (player->GetLevel() >= 20)
|
||||||
nonCombatEngine->addStrategy("bhealth", false);
|
nonCombatEngine->addStrategy("bsanc", false);
|
||||||
else
|
else
|
||||||
nonCombatEngine->addStrategy("bdps", false);
|
nonCombatEngine->addStrategy("bmight", false);
|
||||||
}
|
}
|
||||||
else if (tab == PALADIN_TAB_HOLY)
|
else if (tab == PALADIN_TAB_HOLY)
|
||||||
nonCombatEngine->addStrategiesNoInit("dps assist", "bmana", "bcast", nullptr);
|
nonCombatEngine->addStrategiesNoInit("dps assist", "bwisdom", "bcast", nullptr);
|
||||||
else
|
else
|
||||||
nonCombatEngine->addStrategiesNoInit("dps assist", "bdps", "baoe", nullptr);
|
nonCombatEngine->addStrategiesNoInit("dps assist", "bmight", "baoe", nullptr);
|
||||||
|
|
||||||
nonCombatEngine->addStrategiesNoInit("cure", nullptr);
|
nonCombatEngine->addStrategiesNoInit("cure", nullptr);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -767,6 +767,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI)
|
||||||
|
{
|
||||||
|
if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld())
|
||||||
|
return;
|
||||||
|
|
||||||
|
bot->GetMotionMaster()->Clear();
|
||||||
|
if (resetAI)
|
||||||
|
Reset(true);
|
||||||
|
else
|
||||||
|
InterruptSpell();
|
||||||
|
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
||||||
|
bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0);
|
||||||
|
bot->SendMovementFlagUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerbotAI::HandleTeleportAck()
|
void PlayerbotAI::HandleTeleportAck()
|
||||||
{
|
{
|
||||||
if (!bot || !bot->GetSession())
|
if (!bot || !bot->GetSession())
|
||||||
@ -809,7 +824,7 @@ void PlayerbotAI::HandleTeleportAck()
|
|||||||
bot->StopMoving();
|
bot->StopMoving();
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulate far teleport latency (cmangos-style)
|
// simulate far teleport latency
|
||||||
SetNextCheckDelay(urand(2000, 5000));
|
SetNextCheckDelay(urand(2000, 5000));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -5971,29 +5986,6 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot)
|
|||||||
LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str());
|
LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname)
|
|
||||||
{
|
|
||||||
uint32 bcount = 0;
|
|
||||||
|
|
||||||
if (Group* group = bot->GetGroup())
|
|
||||||
{
|
|
||||||
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
|
|
||||||
{
|
|
||||||
Player* member = gref->GetSource();
|
|
||||||
if (!member || !member->IsInWorld())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!member->IsInSameRaidWith(player))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (HasAura(spellname, member, true))
|
|
||||||
bcount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bcount;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 PlayerbotAI::GetNearGroupMemberCount(float dis)
|
int32 PlayerbotAI::GetNearGroupMemberCount(float dis)
|
||||||
{
|
{
|
||||||
int count = 1; // yourself
|
int count = 1; // yourself
|
||||||
|
|||||||
@ -396,6 +396,7 @@ public:
|
|||||||
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
void HandleMasterIncomingPacket(WorldPacket const& packet);
|
||||||
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
void HandleMasterOutgoingPacket(WorldPacket const& packet);
|
||||||
void HandleTeleportAck();
|
void HandleTeleportAck();
|
||||||
|
void TeleportTo(WorldLocation loc, bool resetAI = false);
|
||||||
void ChangeEngine(BotState type);
|
void ChangeEngine(BotState type);
|
||||||
void ChangeEngineOnCombat();
|
void ChangeEngineOnCombat();
|
||||||
void ChangeEngineOnNonCombat();
|
void ChangeEngineOnNonCombat();
|
||||||
@ -493,7 +494,6 @@ public:
|
|||||||
void ImbueItem(Item* item, Unit* target);
|
void ImbueItem(Item* item, Unit* target);
|
||||||
void ImbueItem(Item* item);
|
void ImbueItem(Item* item);
|
||||||
void EnchantItemT(uint32 spellid, uint8 slot);
|
void EnchantItemT(uint32 spellid, uint8 slot);
|
||||||
uint32 GetBuffedCount(Player* player, std::string const spellname);
|
|
||||||
int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance);
|
int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance);
|
||||||
|
|
||||||
virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr);
|
virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr);
|
||||||
|
|||||||
@ -1697,14 +1697,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bot->GetMotionMaster()->Clear();
|
botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true);
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
||||||
if (botAI)
|
|
||||||
botAI->Reset(true);
|
|
||||||
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
|
||||||
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
|
|
||||||
bot->SendMovementFlagUpdate();
|
|
||||||
|
|
||||||
if (pmo)
|
if (pmo)
|
||||||
pmo->finish();
|
pmo->finish();
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,8 @@ void LootTargetList::shrink(time_t fromTime)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LootObject::LootObject(Player* bot, ObjectGuid guid) : guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0)
|
LootObject::LootObject(Player* bot, ObjectGuid guid)
|
||||||
|
: guid(), skillId(SKILL_NONE), reqSkillValue(0), reqItem(0), isNeededQuestItem(false)
|
||||||
{
|
{
|
||||||
Refresh(bot, guid);
|
Refresh(bot, guid);
|
||||||
}
|
}
|
||||||
@ -55,6 +56,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
skillId = SKILL_NONE;
|
skillId = SKILL_NONE;
|
||||||
reqSkillValue = 0;
|
reqSkillValue = 0;
|
||||||
reqItem = 0;
|
reqItem = 0;
|
||||||
|
isNeededQuestItem = false;
|
||||||
guid.Clear();
|
guid.Clear();
|
||||||
|
|
||||||
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
||||||
@ -101,6 +103,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (IsNeededForQuest(bot, itemId))
|
if (IsNeededForQuest(bot, itemId))
|
||||||
{
|
{
|
||||||
this->guid = lootGUID;
|
this->guid = lootGUID;
|
||||||
|
this->isNeededQuestItem = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +138,21 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (!proto)
|
if (!proto)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Moonpetal Lily, Hyacinth Mushroom etc. expose quest
|
||||||
|
// drops here (not in gameobject_questitem). Flag it so
|
||||||
|
// the INTERACT_COND gate lets the bot through.
|
||||||
|
if (IsNeededForQuest(bot, itemId))
|
||||||
|
{
|
||||||
|
this->guid = lootGUID;
|
||||||
|
this->isNeededQuestItem = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (proto->Class != ITEM_CLASS_QUEST)
|
if (proto->Class != ITEM_CLASS_QUEST)
|
||||||
{
|
{
|
||||||
onlyHasQuestItems = false;
|
onlyHasQuestItems = false;
|
||||||
break;
|
// keep scanning — a later item may be needed
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this item references another loot table, process it
|
// If this item references another loot table, process it
|
||||||
@ -157,11 +171,15 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID)
|
|||||||
if (!refProto)
|
if (!refProto)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (refProto->Class != ITEM_CLASS_QUEST)
|
if (IsNeededForQuest(bot, refItemId))
|
||||||
{
|
{
|
||||||
onlyHasQuestItems = false;
|
this->guid = lootGUID;
|
||||||
break;
|
this->isNeededQuestItem = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refProto->Class != ITEM_CLASS_QUEST)
|
||||||
|
onlyHasQuestItems = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,6 +288,7 @@ LootObject::LootObject(LootObject const& other)
|
|||||||
skillId = other.skillId;
|
skillId = other.skillId;
|
||||||
reqSkillValue = other.reqSkillValue;
|
reqSkillValue = other.reqSkillValue;
|
||||||
reqItem = other.reqItem;
|
reqItem = other.reqItem;
|
||||||
|
isNeededQuestItem = other.isNeededQuestItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LootObject::IsLootPossible(Player* bot)
|
bool LootObject::IsLootPossible(Player* bot)
|
||||||
@ -299,10 +318,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent bot from running to chests that are unlootable (e.g. Gunship Armory before completing the event) or on
|
// Block event-gated chests (Gunship Armory pre-event) and unspawned
|
||||||
// respawn time
|
// GOs. INTERACT_COND alone is allowed when the GO holds a quest
|
||||||
|
// item we need — ConditionMgr already gates on quest state.
|
||||||
GameObject* go = botAI->GetGameObject(guid);
|
GameObject* go = botAI->GetGameObject(guid);
|
||||||
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND | GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
|
if (go && (go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE) || !go->isSpawned()))
|
||||||
|
return false;
|
||||||
|
if (go && go->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_INTERACT_COND) && !isNeededQuestItem)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (skillId == SKILL_NONE)
|
if (skillId == SKILL_NONE)
|
||||||
@ -340,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
|||||||
|
|
||||||
bool LootObjectStack::Add(ObjectGuid guid)
|
bool LootObjectStack::Add(ObjectGuid guid)
|
||||||
{
|
{
|
||||||
|
// expire old completed entries so a despawn/respawn with a reused
|
||||||
|
// guid can still be looted later
|
||||||
|
completedLoot.shrink(time(nullptr) - 300);
|
||||||
|
|
||||||
|
if (completedLoot.find(guid) != completedLoot.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
|
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
|
||||||
{
|
{
|
||||||
availableLoot.shrink(time(nullptr) - 30);
|
availableLoot.shrink(time(nullptr) - 30);
|
||||||
@ -363,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
|
|||||||
availableLoot.erase(i);
|
availableLoot.erase(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LootObjectStack::Clear() { availableLoot.clear(); }
|
void LootObjectStack::MarkCompleted(ObjectGuid guid)
|
||||||
|
{
|
||||||
|
Remove(guid);
|
||||||
|
completedLoot.insert(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LootObjectStack::Clear()
|
||||||
|
{
|
||||||
|
availableLoot.clear();
|
||||||
|
completedLoot.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool LootObjectStack::CanLoot(float maxDistance)
|
bool LootObjectStack::CanLoot(float maxDistance)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -26,7 +26,7 @@ public:
|
|||||||
class LootObject
|
class LootObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LootObject() : skillId(0), reqSkillValue(0), reqItem(0) {}
|
LootObject() : skillId(0), reqSkillValue(0), reqItem(0), isNeededQuestItem(false) {}
|
||||||
LootObject(Player* bot, ObjectGuid guid);
|
LootObject(Player* bot, ObjectGuid guid);
|
||||||
LootObject(LootObject const& other);
|
LootObject(LootObject const& other);
|
||||||
LootObject& operator=(LootObject const& other) = default;
|
LootObject& operator=(LootObject const& other) = default;
|
||||||
@ -40,6 +40,9 @@ public:
|
|||||||
uint32 skillId;
|
uint32 skillId;
|
||||||
uint32 reqSkillValue;
|
uint32 reqSkillValue;
|
||||||
uint32 reqItem;
|
uint32 reqItem;
|
||||||
|
// GO holds a quest item we still need; lets us bypass the
|
||||||
|
// INTERACT_COND blanket reject in the loot path
|
||||||
|
bool isNeededQuestItem;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool IsNeededForQuest(Player* bot, uint32 itemId);
|
static bool IsNeededForQuest(Player* bot, uint32 itemId);
|
||||||
@ -73,6 +76,7 @@ public:
|
|||||||
|
|
||||||
bool Add(ObjectGuid guid);
|
bool Add(ObjectGuid guid);
|
||||||
void Remove(ObjectGuid guid);
|
void Remove(ObjectGuid guid);
|
||||||
|
void MarkCompleted(ObjectGuid guid);
|
||||||
void Clear();
|
void Clear();
|
||||||
bool CanLoot(float maxDistance);
|
bool CanLoot(float maxDistance);
|
||||||
LootObject GetLoot(float maxDistance = 0);
|
LootObject GetLoot(float maxDistance = 0);
|
||||||
@ -82,6 +86,9 @@ private:
|
|||||||
|
|
||||||
Player* bot;
|
Player* bot;
|
||||||
LootTargetList availableLoot;
|
LootTargetList availableLoot;
|
||||||
|
// Guids we already opened loot on; blocks "add all loot" from
|
||||||
|
// re-adding the same corpse before it despawns.
|
||||||
|
LootTargetList completedLoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
#include "ChatHelper.h"
|
#include "ChatHelper.h"
|
||||||
#include "MapCollisionData.h"
|
#include "MapCollisionData.h"
|
||||||
#include "MapMgr.h"
|
#include "MapMgr.h"
|
||||||
|
#include "ModelIgnoreFlags.h"
|
||||||
#include "PathGenerator.h"
|
#include "PathGenerator.h"
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
#include "RaceMgr.h"
|
#include "RaceMgr.h"
|
||||||
@ -681,93 +682,6 @@ std::vector<WorldPosition> WorldPosition::frommGridCoord(mGridCoord GridCoord)
|
|||||||
return retVec;
|
return retVec;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Cleanup — make this actually work.
|
|
||||||
void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y)
|
|
||||||
{
|
|
||||||
std::string const fileName = "load_map_grid.csv";
|
|
||||||
/*
|
|
||||||
if (isOverworld() && false || false)
|
|
||||||
{
|
|
||||||
if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y))
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This needs to be disabled or maps will not load.
|
|
||||||
// Needs more testing to check for impact on movement.
|
|
||||||
if (false)
|
|
||||||
if (!TravelMgr::instance().isBadVmap(mapId, x, y))
|
|
||||||
{
|
|
||||||
// load VMAPs for current map/grid...
|
|
||||||
const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId);
|
|
||||||
//const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal.
|
|
||||||
|
|
||||||
int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap(
|
|
||||||
(sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y);
|
|
||||||
switch (vmapLoadResult)
|
|
||||||
{
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_OK:
|
|
||||||
// LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
|
|
||||||
// mapName, mapId, x, y, x, y);
|
|
||||||
break;
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_ERROR:
|
|
||||||
// LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{},
|
|
||||||
// y:{})", mapName, mapId, x, y, x, y);
|
|
||||||
TravelMgr::instance().addBadVmap(mapId, x, y);
|
|
||||||
break;
|
|
||||||
case VMAP::VMAP_LOAD_RESULT_IGNORED:
|
|
||||||
TravelMgr::instance().addBadVmap(mapId, x, y);
|
|
||||||
// LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})",
|
|
||||||
// mapName, mapId, x, y, x, y);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (!TravelMgr::instance().isBadMmap(mapId, x, y))
|
|
||||||
{
|
|
||||||
// load navmesh
|
|
||||||
Map* map = getMap();
|
|
||||||
if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR)
|
|
||||||
TravelMgr::instance().addBadMmap(mapId, x, y);
|
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog(fileName))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr();
|
|
||||||
out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1")
|
|
||||||
<< ",";
|
|
||||||
printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true);
|
|
||||||
sPlayerbotAIConfig.log(fileName, out.str().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WorldPosition::loadMapAndVMaps(WorldPosition secondPos)
|
|
||||||
{
|
|
||||||
for (auto& grid : getmGridCoords(secondPos))
|
|
||||||
{
|
|
||||||
loadMapAndVMap(GetMapId(), grid.first, grid.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
|
std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vector3> path)
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> retVec;
|
std::vector<WorldPosition> retVec;
|
||||||
@ -780,39 +694,107 @@ std::vector<WorldPosition> WorldPosition::fromPointsArray(std::vector<G3D::Vecto
|
|||||||
// A single pathfinding attempt from one position to another. Returns pathfinding status and path.
|
// A single pathfinding attempt from one position to another. Returns pathfinding status and path.
|
||||||
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
|
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot)
|
||||||
{
|
{
|
||||||
if (!bot)
|
Unit* pathUnit = bot;
|
||||||
|
Creature* tempCreature = nullptr;
|
||||||
|
|
||||||
|
if (!pathUnit)
|
||||||
|
{
|
||||||
|
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
|
||||||
|
if (!map)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
// Load mmaps and vmaps between the two points.
|
tempCreature = new Creature();
|
||||||
loadMapAndVMaps(startPos);
|
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
|
||||||
|
PHASEMASK_NORMAL, 1 /*entry*/, 0,
|
||||||
|
startPos.GetPositionX(), startPos.GetPositionY(),
|
||||||
|
startPos.GetPositionZ(), 0))
|
||||||
|
{
|
||||||
|
delete tempCreature;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
pathUnit = tempCreature;
|
||||||
|
|
||||||
PathGenerator path(bot);
|
map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY()));
|
||||||
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ());
|
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PathGenerator path(pathUnit);
|
||||||
|
// Source is a temp Creature, so CreateFilter's bot block doesn't
|
||||||
|
// fire — apply the same bot cost biases here so generated paths
|
||||||
|
// match what bots prefer at runtime (STEEP/water are reachable
|
||||||
|
// but not preferred).
|
||||||
|
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
|
||||||
|
path.SetNavTerrainCost(NAV_WATER, 10.0f);
|
||||||
|
auto result = getPathStepFrom(startPos, path);
|
||||||
|
|
||||||
|
if (tempCreature)
|
||||||
|
delete tempCreature;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathfinder-reuse overload — caller owns the PathGenerator and any
|
||||||
|
// per-call configuration. Used by getPathFromPath to thread one
|
||||||
|
// PathGenerator through the whole 40-step chain instead of
|
||||||
|
// constructing a new one per step.
|
||||||
|
std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path)
|
||||||
|
{
|
||||||
|
// Explicit-start overload. Without this, the chain begins from the
|
||||||
|
// unit's current position every step and never advances.
|
||||||
|
path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
|
||||||
|
GetPositionX(), GetPositionY(), GetPositionZ(), false);
|
||||||
|
|
||||||
Movement::PointsArray points = path.GetPath();
|
Movement::PointsArray points = path.GetPath();
|
||||||
PathType type = path.GetPathType();
|
PathType type = path.GetPathType();
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv"))
|
// PathType is a bitmask. AC's PathGenerator returns
|
||||||
{
|
// NORMAL | NOT_USING_PATH when start/end poly is INVALID_POLYREF
|
||||||
std::ostringstream out;
|
// (BuildShortcut produces a 2-point straight line through whatever's
|
||||||
out << std::fixed << std::setprecision(1);
|
// in the way). Reject those to avoid silently dispatching a
|
||||||
printWKT({startPos, *this}, out);
|
// geometry-ignoring shortcut.
|
||||||
sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str());
|
if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
|
||||||
}
|
(type & PATHFIND_NOT_USING_PATH))
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL))
|
|
||||||
{
|
|
||||||
std::ostringstream out;
|
|
||||||
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
|
|
||||||
out << std::fixed << std::setprecision(1) << type << ",";
|
|
||||||
printWKT(fromPointsArray(points), out, 1);
|
|
||||||
sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
|
|
||||||
return fromPointsArray(points);
|
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
std::vector<WorldPosition> retvec = fromPointsArray(points);
|
||||||
|
|
||||||
|
// Underwater path-extension. When PATHFIND_INCOMPLETE ends within
|
||||||
|
// 50y of dest and both endpoints are underwater with LOS, extend
|
||||||
|
// by one 5y step (or straight to dest if <5y). Lets bots traverse
|
||||||
|
// navmesh-poor water volumes.
|
||||||
|
if (type & PATHFIND_INCOMPLETE)
|
||||||
|
{
|
||||||
|
WorldPosition end = *this;
|
||||||
|
WorldPosition lastPoint = retvec.back();
|
||||||
|
float dist = lastPoint.distance(&end);
|
||||||
|
|
||||||
|
if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater())
|
||||||
|
{
|
||||||
|
Map* m = end.getMap();
|
||||||
|
bool inLos = m && m->isInLineOfSight(
|
||||||
|
lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f,
|
||||||
|
end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f,
|
||||||
|
PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
|
||||||
|
if (inLos)
|
||||||
|
{
|
||||||
|
if (dist < 5.0f)
|
||||||
|
retvec.push_back(end);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float dx = end.GetPositionX() - lastPoint.GetPositionX();
|
||||||
|
float dy = end.GetPositionY() - lastPoint.GetPositionY();
|
||||||
|
float dz = end.GetPositionZ() - lastPoint.GetPositionZ();
|
||||||
|
float scale = 5.0f / dist;
|
||||||
|
retvec.emplace_back(end.GetMapId(),
|
||||||
|
lastPoint.GetPositionX() + dx * scale,
|
||||||
|
lastPoint.GetPositionY() + dy * scale,
|
||||||
|
lastPoint.GetPositionZ() + dz * scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retvec;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
|
bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
|
||||||
@ -848,27 +830,63 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
|
|||||||
|
|
||||||
std::vector<WorldPosition> subPath, fullPath = startPath;
|
std::vector<WorldPosition> subPath, fullPath = startPath;
|
||||||
|
|
||||||
|
// Construct ONE PathGenerator and thread it through every step
|
||||||
|
// to avoid the per-step alloc cost. AC's BuildPolyPath has a
|
||||||
|
// subpath-prefix optimization that can bend chained probes, so
|
||||||
|
// call Clear() before each step to reset the poly cache.
|
||||||
|
Unit* pathUnit = bot;
|
||||||
|
Creature* tempCreature = nullptr;
|
||||||
|
if (!pathUnit)
|
||||||
|
{
|
||||||
|
Map* map = sMapMgr->FindBaseMap(GetMapId());
|
||||||
|
if (!map)
|
||||||
|
return fullPath;
|
||||||
|
|
||||||
|
tempCreature = new Creature();
|
||||||
|
if (!tempCreature->Create(map->GenerateLowGuid<HighGuid::Unit>(), map,
|
||||||
|
PHASEMASK_NORMAL, 1 /*entry*/, 0,
|
||||||
|
currentPos.GetPositionX(), currentPos.GetPositionY(),
|
||||||
|
currentPos.GetPositionZ(), 0))
|
||||||
|
{
|
||||||
|
delete tempCreature;
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
pathUnit = tempCreature;
|
||||||
|
map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY()));
|
||||||
|
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PathGenerator path(pathUnit);
|
||||||
|
// Same reason as getPathStepFrom: temp-Creature source doesn't trip
|
||||||
|
// CreateFilter's bot block, so apply the bot cost biases manually.
|
||||||
|
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
|
||||||
|
path.SetNavTerrainCost(NAV_WATER, 10.0f);
|
||||||
|
|
||||||
// Limit the pathfinding attempts
|
// Limit the pathfinding attempts
|
||||||
for (uint32 i = 0; i < maxAttempt; i++)
|
for (uint32 i = 0; i < maxAttempt; i++)
|
||||||
{
|
{
|
||||||
// Try to pathfind to this position.
|
// Reset cached poly state from the previous step so each call
|
||||||
subPath = getPathStepFrom(currentPos, bot);
|
// is a fresh A* (otherwise the prefix-recycling at
|
||||||
|
// PathGenerator.cpp BuildPolyPath snaps the start to the
|
||||||
|
// cached corridor, bending the chain).
|
||||||
|
path.Clear();
|
||||||
|
|
||||||
|
subPath = getPathStepFrom(currentPos, path);
|
||||||
|
|
||||||
// If we could not find a path return what we have now.
|
|
||||||
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
|
if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Append the path excluding the start (this should be the same as the end of the startPath)
|
|
||||||
fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
|
fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
|
||||||
|
|
||||||
// Are we there yet?
|
|
||||||
if (isPathTo(subPath))
|
if (isPathTo(subPath))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Continue pathfinding.
|
|
||||||
currentPos = subPath.back();
|
currentPos = subPath.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tempCreature)
|
||||||
|
delete tempCreature;
|
||||||
|
|
||||||
return fullPath;
|
return fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1073,6 +1091,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData)
|
|||||||
loadedFromDB = true;
|
loadedFromDB = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TravelDestination::~TravelDestination()
|
||||||
|
{
|
||||||
|
for (WorldPosition* point : points)
|
||||||
|
delete point;
|
||||||
|
|
||||||
|
points.clear();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
|
std::vector<WorldPosition*> TravelDestination::getPoints(bool ignoreFull)
|
||||||
{
|
{
|
||||||
if (ignoreFull)
|
if (ignoreFull)
|
||||||
@ -2379,9 +2405,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
|
sPlayerbotAIConfig.openLog("unload_grid.csv", "w");
|
||||||
sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
|
sPlayerbotAIConfig.openLog("unload_obj.csv", "w");
|
||||||
|
|
||||||
TravelNodeMap::instance().loadNodeStore();
|
// Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init().
|
||||||
|
|
||||||
TravelNodeMap::instance().generateAll();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
bool fullNavPointReload = false;
|
bool fullNavPointReload = false;
|
||||||
@ -2772,7 +2796,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
|
//if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode))
|
||||||
// continue;
|
// continue;
|
||||||
|
|
||||||
startNode->buildPath(endNode, nullptr, false);
|
startNode->BuildPath(endNode, nullptr, false);
|
||||||
|
|
||||||
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
|
//if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete())
|
||||||
//startNode->removeLinkTo(endNode);
|
//startNode->removeLinkTo(endNode);
|
||||||
@ -2896,7 +2920,7 @@ void TravelMgr::LoadQuestTravelTable()
|
|||||||
|
|
||||||
TravelNodePath nodePath = *path.second;
|
TravelNodePath nodePath = *path.second;
|
||||||
|
|
||||||
std::vector<WorldPosition> pPath = nodePath.getPath();
|
std::vector<WorldPosition> pPath = nodePath.GetPath();
|
||||||
std::reverse(pPath.begin(), pPath.end());
|
std::reverse(pPath.begin(), pPath.end());
|
||||||
|
|
||||||
nodePath.setPath(pPath);
|
nodePath.setPath(pPath);
|
||||||
@ -4359,8 +4383,7 @@ void TravelMgr::Init()
|
|||||||
PrepareZone2LevelBracket();
|
PrepareZone2LevelBracket();
|
||||||
PrepareDestinationCache();
|
PrepareDestinationCache();
|
||||||
}
|
}
|
||||||
sTravelNodeMap.InitTaxiGraph();
|
sTravelNodeMap.Init();
|
||||||
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
|
||||||
@ -4407,7 +4430,7 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
|
|||||||
std::vector<std::vector<uint32>> validDestinations;
|
std::vector<std::vector<uint32>> validDestinations;
|
||||||
|
|
||||||
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
|
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
|
||||||
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f)
|
if (!nearestFlightMaster)
|
||||||
return validDestinations;
|
return validDestinations;
|
||||||
|
|
||||||
uint32 fromNode = nearestFlightMaster->taxiNodeId;
|
uint32 fromNode = nearestFlightMaster->taxiNodeId;
|
||||||
@ -4426,9 +4449,9 @@ std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player*
|
|||||||
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
|
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
|
||||||
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
|
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
|
||||||
|
|
||||||
//Simplify destination delection. Its either target cities (Based on config value) or target world.
|
|
||||||
std::vector<uint32> candidateZones;
|
std::vector<uint32> candidateZones;
|
||||||
if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
|
if (botLevel >= 10 && !botInCapital &&
|
||||||
|
urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
|
||||||
{
|
{
|
||||||
TeamId botTeam = bot->GetTeamId();
|
TeamId botTeam = bot->GetTeamId();
|
||||||
for (Capital const& capital : capitals)
|
for (Capital const& capital : capitals)
|
||||||
@ -4555,6 +4578,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
|
|||||||
return fallbackLocations;
|
return fallbackLocations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer)
|
||||||
|
{
|
||||||
|
uint16 botMapId = bot->GetMapId();
|
||||||
|
auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache;
|
||||||
|
|
||||||
|
auto mapIt = cache.find(botMapId);
|
||||||
|
if (mapIt == cache.end() || mapIt->second.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Collect all areas on this map that have auctioneers
|
||||||
|
std::vector<uint32> areaIds;
|
||||||
|
areaIds.reserve(mapIt->second.size());
|
||||||
|
for (auto const& [areaId, npcs] : mapIt->second)
|
||||||
|
{
|
||||||
|
if (!npcs.empty())
|
||||||
|
areaIds.push_back(areaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Pick a random area, then a random auctioneer in that area
|
||||||
|
uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)];
|
||||||
|
auto const& auctioneers = mapIt->second.at(selectedArea);
|
||||||
|
outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void TravelMgr::PrepareZone2LevelBracket()
|
void TravelMgr::PrepareZone2LevelBracket()
|
||||||
{
|
{
|
||||||
// Classic WoW - starter zones
|
// Classic WoW - starter zones
|
||||||
@ -4641,6 +4692,7 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
uint32 flightMastersCount = 0;
|
uint32 flightMastersCount = 0;
|
||||||
uint32 innkeepersCount = 0;
|
uint32 innkeepersCount = 0;
|
||||||
uint32 bankerCount = 0;
|
uint32 bankerCount = 0;
|
||||||
|
uint32 auctioneerCount = 0;
|
||||||
|
|
||||||
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
|
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
|
||||||
// Temporary map to group creatures by entry and area
|
// Temporary map to group creatures by entry and area
|
||||||
@ -4687,18 +4739,18 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
(creatureTemplate->unit_flags & 4096) == 0 &&
|
(creatureTemplate->unit_flags & 4096) == 0 &&
|
||||||
creatureTemplate->rank == 0)
|
creatureTemplate->rank == 0)
|
||||||
{
|
{
|
||||||
int32 roundX = static_cast<int32>(std::lround(x / 50.0f));
|
uint32 roundX = static_cast<uint32>(std::round(x / 50.0f));
|
||||||
int32 roundY = static_cast<int32>(std::lround(y / 50.0f));
|
uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
|
||||||
int32 roundZ = static_cast<int32>(std::lround(z / 50.0f));
|
uint32 roundZ = static_cast<uint32>(std::round(z / 50.0f));
|
||||||
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
|
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
|
||||||
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
|
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
|
||||||
}
|
}
|
||||||
// FLIGHT MASTERS
|
// FLIGHT MASTERS
|
||||||
// Entry 29480 is Grimwing (Storm Peaks)
|
// Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but
|
||||||
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
|
// isn't a real usable flight master; skip it.
|
||||||
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
|
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
|
||||||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
|
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
|
||||||
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
|
creatureTemplate->Entry != 29480)
|
||||||
{
|
{
|
||||||
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
||||||
bool forHorde = !(factionEntry->hostileMask & 4);
|
bool forHorde = !(factionEntry->hostileMask & 4);
|
||||||
@ -4783,7 +4835,7 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
|
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
|
||||||
creatureTemplate->Entry != 29282)
|
creatureTemplate->Entry != 29282)
|
||||||
{
|
{
|
||||||
BankerLocation bLoc;
|
NpcLocation bLoc;
|
||||||
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
|
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
|
||||||
bLoc.entry = templateEntry;
|
bLoc.entry = templateEntry;
|
||||||
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
||||||
@ -4806,6 +4858,31 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
}
|
}
|
||||||
bankerCount++;
|
bankerCount++;
|
||||||
}
|
}
|
||||||
|
// === AUCTIONEERS ===
|
||||||
|
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER)
|
||||||
|
{
|
||||||
|
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
|
||||||
|
if (!factionEntry)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool forHorde = !(factionEntry->hostileMask & 4);
|
||||||
|
bool forAlliance = !(factionEntry->hostileMask & 2);
|
||||||
|
|
||||||
|
if (!forHorde && !forAlliance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
NpcLocation aLoc;
|
||||||
|
aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI);
|
||||||
|
aLoc.entry = templateEntry;
|
||||||
|
|
||||||
|
if (forHorde)
|
||||||
|
hordeAuctioneerCache[mapId][areaId].push_back(aLoc);
|
||||||
|
|
||||||
|
if (forAlliance)
|
||||||
|
allianceAuctioneerCache[mapId][areaId].push_back(aLoc);
|
||||||
|
|
||||||
|
auctioneerCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process temporary caches
|
// Process temporary caches
|
||||||
@ -4815,16 +4892,29 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
{
|
{
|
||||||
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
|
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
|
||||||
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
|
||||||
|
|
||||||
|
float totalX = 0.0f;
|
||||||
|
float totalY = 0.0f;
|
||||||
|
float totalZ = 0.0f;
|
||||||
|
for (CreatureData const& creatureData : creatureDataList)
|
||||||
|
{
|
||||||
|
totalX += creatureData.posX;
|
||||||
|
totalY += creatureData.posY;
|
||||||
|
totalZ += creatureData.posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
float avgX = totalX / creatureDataList.size();
|
||||||
|
float avgY = totalY / creatureDataList.size();
|
||||||
|
float avgZ = totalZ / creatureDataList.size();
|
||||||
|
uint32 mapId = std::get<0>(gridTuple);
|
||||||
|
|
||||||
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
|
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
|
||||||
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
|
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
|
||||||
{
|
{
|
||||||
if (l < 1 || l > maxLevel)
|
if (l < 1 || l > maxLevel)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple),
|
locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f));
|
||||||
static_cast<float>(std::get<1>(gridTuple)) * 50.0f,
|
|
||||||
static_cast<float>(std::get<2>(gridTuple)) * 50.0f,
|
|
||||||
static_cast<float>(std::get<3>(gridTuple)) * 50.0f));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4870,5 +4960,5 @@ void TravelMgr::PrepareDestinationCache()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount);
|
LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#define _PLAYERBOT_TRAVELMGR_H
|
#define _PLAYERBOT_TRAVELMGR_H
|
||||||
|
|
||||||
#include <boost/functional/hash.hpp>
|
#include <boost/functional/hash.hpp>
|
||||||
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
class Creature;
|
class Creature;
|
||||||
class GuidPosition;
|
class GuidPosition;
|
||||||
|
class PathGenerator;
|
||||||
class ObjectGuid;
|
class ObjectGuid;
|
||||||
class Quest;
|
class Quest;
|
||||||
class Player;
|
class Player;
|
||||||
@ -268,12 +270,6 @@ public:
|
|||||||
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
|
std::vector<mGridCoord> getmGridCoords(WorldPosition secondPos);
|
||||||
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
std::vector<WorldPosition> frommGridCoord(mGridCoord GridCoord);
|
||||||
|
|
||||||
void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y);
|
|
||||||
|
|
||||||
void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); }
|
|
||||||
|
|
||||||
void loadMapAndVMaps(WorldPosition secondPos);
|
|
||||||
|
|
||||||
// Display functions
|
// Display functions
|
||||||
WorldPosition getDisplayLocation();
|
WorldPosition getDisplayLocation();
|
||||||
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; }
|
||||||
@ -288,6 +284,7 @@ public:
|
|||||||
|
|
||||||
// Pathfinding
|
// Pathfinding
|
||||||
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
|
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
|
||||||
|
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder);
|
||||||
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
|
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
|
||||||
|
|
||||||
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)
|
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)
|
||||||
@ -297,10 +294,26 @@ public:
|
|||||||
|
|
||||||
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
|
std::vector<WorldPosition> getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); }
|
||||||
|
|
||||||
bool isPathTo(std::vector<WorldPosition> path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance)
|
// The path "reaches" this position when its last point is on
|
||||||
|
// the same map, within maxDistance horizontally, and within
|
||||||
|
// maxZDistance vertically. 3D Euclidean distance would falsely
|
||||||
|
// accept paths that end the right horizontal distance from us
|
||||||
|
// but on a roof/floor below. maxDistance == 0 falls back to
|
||||||
|
// targetPosRecalcDistance (0.1y).
|
||||||
|
bool isPathTo(std::vector<WorldPosition> const& path, float const maxDistance = 0.0f,
|
||||||
|
float const maxZDistance = 2.0f) const
|
||||||
{
|
{
|
||||||
return !path.empty() && distance(path.back()) < maxDistance;
|
if (path.empty())
|
||||||
};
|
return false;
|
||||||
|
WorldPosition const& back = path.back();
|
||||||
|
if (back.GetMapId() != GetMapId())
|
||||||
|
return false;
|
||||||
|
float const realMax = maxDistance > 0.0f ? maxDistance
|
||||||
|
: sPlayerbotAIConfig.targetPosRecalcDistance;
|
||||||
|
if (GetExactDist2dSq(&back) >= realMax * realMax)
|
||||||
|
return false;
|
||||||
|
return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance;
|
||||||
|
}
|
||||||
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
|
bool cropPathTo(std::vector<WorldPosition>& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance);
|
||||||
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); }
|
||||||
|
|
||||||
@ -507,9 +520,15 @@ public:
|
|||||||
radiusMin = radiusMin1;
|
radiusMin = radiusMin1;
|
||||||
radiusMax = radiusMax1;
|
radiusMax = radiusMax1;
|
||||||
}
|
}
|
||||||
virtual ~TravelDestination() = default;
|
virtual ~TravelDestination();
|
||||||
|
|
||||||
void addPoint(WorldPosition* pos) { points.push_back(pos); }
|
void addPoint(WorldPosition* pos)
|
||||||
|
{
|
||||||
|
if (!pos)
|
||||||
|
return;
|
||||||
|
|
||||||
|
points.push_back(new WorldPosition(*pos));
|
||||||
|
}
|
||||||
|
|
||||||
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
void setExpireDelay(uint32 delay) { expireDelay = delay; }
|
||||||
|
|
||||||
@ -673,7 +692,7 @@ public:
|
|||||||
bool isActive(Player* bot) override;
|
bool isActive(Player* bot) override;
|
||||||
virtual CreatureTemplate const* GetCreatureTemplate();
|
virtual CreatureTemplate const* GetCreatureTemplate();
|
||||||
std::string const getName() override { return "RpgTravelDestination"; }
|
std::string const getName() override { return "RpgTravelDestination"; }
|
||||||
int32 getEntry() override { return 0; }
|
int32 getEntry() override { return entry; }
|
||||||
std::string const getTitle() override;
|
std::string const getTitle() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@ -985,18 +1004,14 @@ private:
|
|||||||
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BankerLocation
|
|
||||||
{
|
|
||||||
WorldLocation loc;
|
|
||||||
uint32 entry;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation caches
|
// Navigation caches
|
||||||
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
|
||||||
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
|
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
|
||||||
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
|
std::map<uint8, std::vector<NpcLocation>> bankerLocsPerLevelCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> hordeAuctioneerCache;
|
||||||
|
std::unordered_map<uint16, std::unordered_map<uint32, std::vector<NpcLocation>>> allianceAuctioneerCache;
|
||||||
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
|
||||||
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
|
||||||
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,12 +8,12 @@
|
|||||||
|
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
|
||||||
|
#include "G3D/Vector3.h"
|
||||||
#include "TravelMgr.h"
|
#include "TravelMgr.h"
|
||||||
|
|
||||||
// THEORY
|
// THEORY
|
||||||
//
|
//
|
||||||
// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase.
|
// Pathfinding uses the detour recast navmesh engine for mob, npc, and bot movement.
|
||||||
// This system is used for mob and npc pathfinding and in this codebase also for bots.
|
|
||||||
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
// Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y.
|
||||||
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
// This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a
|
||||||
// straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't
|
// straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't
|
||||||
@ -24,56 +24,90 @@
|
|||||||
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
|
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
|
||||||
//
|
//
|
||||||
// Bot at <S> wants to move to <E>
|
// Bot at <S> wants to move to <E>
|
||||||
// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
// [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not
|
||||||
// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
// from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel.
|
||||||
//
|
//
|
||||||
// Termonology:
|
// Terminology:
|
||||||
// Node: a location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
// Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach
|
||||||
// other nodes. Link: the connection between two nodes. A link signifies that the bot can travel from one node to
|
// other nodes. Stored in DB table `playerbots_travelnode`.
|
||||||
// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one
|
// Link: The connection between two nodes. A link signifies that the bot can travel from one node to another.
|
||||||
// node (or position) to another. A path can be imcomplete or empty which means there is no link. Route: the list of
|
// A link is one-directional. Stored in `playerbots_travelnode_link`.
|
||||||
// nodes that give the shortest route from a node to a distant node. Routes are calculated using a standard A* search
|
// Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another.
|
||||||
// based on links.
|
// A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`.
|
||||||
|
// Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using
|
||||||
|
// a standard A* search based on links.
|
||||||
//
|
//
|
||||||
// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future
|
// Edge types (TravelNodePathType):
|
||||||
// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this
|
// walk(1) — Walk via navmesh waypoints (stored in DB)
|
||||||
// requires a locking mechanism.
|
// areaTrigger(2) — AreaTrigger teleport (auto-discovered at startup)
|
||||||
|
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
|
||||||
|
// flightPath(4) — Taxi flight between flight masters
|
||||||
|
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
|
||||||
|
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
|
||||||
|
//
|
||||||
|
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
|
||||||
|
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
|
||||||
|
// a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at
|
||||||
|
// startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock
|
||||||
|
// caused 100-250ms contention spikes against bot threads.
|
||||||
//
|
//
|
||||||
// Initially the current nodes have been made:
|
// Initially the current nodes have been made:
|
||||||
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
// Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route
|
||||||
// calculation) WorldBosses and Unique bosses in instances (These are a logical places bots might want to go in
|
// calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in
|
||||||
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
// instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with
|
||||||
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
// teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators
|
||||||
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
// (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is
|
||||||
// good for global coverage)
|
// good for global coverage).
|
||||||
//
|
//
|
||||||
// To increase coverage/linking extra nodes can be automatically be created.
|
// To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode"
|
||||||
// Current implentation places nodes on paths (including complete) at sub-zone transitions or randomly.
|
// console command after importing the specified node. Current implementation places nodes on paths (including
|
||||||
// After calculating possible links the node is removed if it does not create local coverage.
|
// complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it
|
||||||
|
// does not create local coverage (.fullgenerate only).
|
||||||
//
|
//
|
||||||
|
// Travel Flow:
|
||||||
|
//
|
||||||
|
// GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then
|
||||||
|
// BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight).
|
||||||
|
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
|
||||||
|
// Cross-map travel is handled naturally by portal/transport edges in the A* graph.
|
||||||
|
//
|
||||||
|
// If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination
|
||||||
|
// as a fallback.
|
||||||
|
//
|
||||||
|
// The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item.
|
||||||
|
//
|
||||||
|
// Thread Safety:
|
||||||
|
//
|
||||||
|
// The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still
|
||||||
|
// exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no
|
||||||
|
// runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup
|
||||||
|
// (saveNodeStore) and by the debug dump command.
|
||||||
|
//
|
||||||
|
|
||||||
|
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
|
||||||
|
|
||||||
enum class TravelNodePathType : uint8
|
enum class TravelNodePathType : uint8
|
||||||
{
|
{
|
||||||
none = 0,
|
none = 0,
|
||||||
walk = 1,
|
walk = 1,
|
||||||
portal = 2,
|
areaTrigger = 2,
|
||||||
transport = 3,
|
transport = 3,
|
||||||
flightPath = 4,
|
flightPath = 4,
|
||||||
teleportSpell = 5
|
// value 5 (teleportSpell) reserved — no generator emits it and no
|
||||||
|
// consumer handles it. Re-add when a teleport-spell edge generator
|
||||||
|
// / executor handler returns.
|
||||||
|
staticPortal = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
// A connection between two nodes.
|
// A connection between two nodes.
|
||||||
class TravelNodePath
|
class TravelNodePath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Legacy Constructor for travelnodestore
|
|
||||||
// TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 =
|
|
||||||
// false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0,
|
|
||||||
// float swimDistance1 = 0, bool flightPath1 = false);
|
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk,
|
TravelNodePath(float distance = 0.1f, float extraCost = 0,
|
||||||
uint32 pathObject = 0, bool calculated = false, std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
uint8 pathType = (uint8)TravelNodePathType::walk,
|
||||||
|
uint32 pathObject = 0, bool calculated = false,
|
||||||
|
std::vector<uint8> maxLevelCreature = {0, 0, 0},
|
||||||
float swimDistance = 0)
|
float swimDistance = 0)
|
||||||
: extraCost(extraCost),
|
: extraCost(extraCost),
|
||||||
calculated(calculated),
|
calculated(calculated),
|
||||||
@ -85,7 +119,7 @@ public:
|
|||||||
{
|
{
|
||||||
if (pathType != (uint8)TravelNodePathType::walk)
|
if (pathType != (uint8)TravelNodePathType::walk)
|
||||||
complete = true;
|
complete = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
TravelNodePath(TravelNodePath* basePath)
|
TravelNodePath(TravelNodePath* basePath)
|
||||||
{
|
{
|
||||||
@ -98,11 +132,11 @@ public:
|
|||||||
swimDistance = basePath->swimDistance;
|
swimDistance = basePath->swimDistance;
|
||||||
pathType = basePath->pathType;
|
pathType = basePath->pathType;
|
||||||
pathObject = basePath->pathObject;
|
pathObject = basePath->pathObject;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
bool getComplete() { return complete || pathType != TravelNodePathType::walk; }
|
||||||
std::vector<WorldPosition> getPath() { return path; }
|
std::vector<WorldPosition> GetPath() { return path; }
|
||||||
|
|
||||||
TravelNodePathType getPathType() { return pathType; }
|
TravelNodePathType getPathType() { return pathType; }
|
||||||
uint32 getPathObject() { return pathObject; }
|
uint32 getPathObject() { return pathObject; }
|
||||||
@ -130,9 +164,6 @@ public:
|
|||||||
extraCost = distance / speed;
|
extraCost = distance / speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; }
|
|
||||||
// void setTransport(bool transport1) { transport = transport1; }
|
|
||||||
|
|
||||||
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
void setPathType(TravelNodePathType pathType1) { pathType = pathType1; }
|
||||||
|
|
||||||
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
void setPathObject(uint32 pathObject1) { pathObject = pathObject1; }
|
||||||
@ -186,9 +217,10 @@ class TravelNode
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Constructors
|
// Constructors
|
||||||
TravelNode(){};
|
TravelNode() {}
|
||||||
|
|
||||||
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false)
|
TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node",
|
||||||
|
bool important1 = false)
|
||||||
{
|
{
|
||||||
nodeName = nodeName1;
|
nodeName = nodeName1;
|
||||||
point = point1;
|
point = point1;
|
||||||
@ -207,11 +239,11 @@ public:
|
|||||||
void setPoint(WorldPosition point1) { point = point1; }
|
void setPoint(WorldPosition point1) { point = point1; }
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
std::string const getName() { return nodeName; };
|
std::string const getName() { return nodeName; }
|
||||||
WorldPosition* getPosition() { return &point; };
|
WorldPosition* getPosition() { return &point; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
std::unordered_map<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
|
||||||
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
std::unordered_map<TravelNode*, TravelNodePath*>* getLinks() { return &links; }
|
||||||
bool isImportant() { return important; };
|
bool isImportant() { return important; }
|
||||||
bool isLinked() { return linked; }
|
bool isLinked() { return linked; }
|
||||||
|
|
||||||
bool isTransport()
|
bool isTransport()
|
||||||
@ -235,9 +267,9 @@ public:
|
|||||||
bool isPortal()
|
bool isPortal()
|
||||||
{
|
{
|
||||||
for (auto const& link : *getLinks())
|
for (auto const& link : *getLinks())
|
||||||
if (link.second->getPathType() == TravelNodePathType::portal)
|
if (link.second->getPathType() == TravelNodePathType::areaTrigger ||
|
||||||
|
link.second->getPathType() == TravelNodePathType::staticPortal)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,17 +283,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WorldLocation shortcuts
|
// WorldLocation shortcuts
|
||||||
uint32 getMapId() { return point.GetMapId(); }
|
uint32 GetMapId() { return point.GetMapId(); }
|
||||||
float getX() { return point.GetPositionX(); }
|
float getX() { return point.GetPositionX(); }
|
||||||
float getY() { return point.GetPositionY(); }
|
float getY() { return point.GetPositionY(); }
|
||||||
float getZ() { return point.GetPositionZ(); }
|
float getZ() { return point.GetPositionZ(); }
|
||||||
float getO() { return point.GetOrientation(); }
|
float getO() { return point.GetOrientation(); }
|
||||||
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
float getDistance(WorldPosition pos) { return point.distance(pos); }
|
||||||
float getDistance(TravelNode* node) { return point.distance(node->getPosition()); }
|
float getDistance(TravelNode* node)
|
||||||
float fDist(TravelNode* node) { return point.fDist(node->getPosition()); }
|
{
|
||||||
|
return point.distance(node->getPosition());
|
||||||
|
}
|
||||||
|
float fDist(TravelNode* node)
|
||||||
|
{
|
||||||
|
return point.fDist(node->getPosition());
|
||||||
|
}
|
||||||
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
float fDist(WorldPosition pos) { return point.fDist(pos); }
|
||||||
|
|
||||||
TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true)
|
TravelNodePath* setPathTo(TravelNode* node,
|
||||||
|
TravelNodePath path = TravelNodePath(),
|
||||||
|
bool isLink = true)
|
||||||
{
|
{
|
||||||
if (this != node)
|
if (this != node)
|
||||||
{
|
{
|
||||||
@ -275,10 +315,20 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); }
|
bool hasPathTo(TravelNode* node)
|
||||||
TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; }
|
{
|
||||||
bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); }
|
return paths.find(node) != paths.end();
|
||||||
TravelNodePath* buildPath(TravelNode* endNode, Unit* bot, bool postProcess = false);
|
}
|
||||||
|
TravelNodePath* getPathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return &paths[node];
|
||||||
|
}
|
||||||
|
bool hasCompletePathTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return hasPathTo(node) && getPathTo(node)->getComplete();
|
||||||
|
}
|
||||||
|
TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot,
|
||||||
|
bool postProcess = false);
|
||||||
|
|
||||||
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
void setLinkTo(TravelNode* node, float distance = 0.1f)
|
||||||
{
|
{
|
||||||
@ -291,9 +341,18 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); }
|
bool hasLinkTo(TravelNode* node)
|
||||||
float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
{
|
||||||
float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); }
|
return links.find(node) != links.end();
|
||||||
|
}
|
||||||
|
float linkCostTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
|
float linkDistanceTo(TravelNode* node)
|
||||||
|
{
|
||||||
|
return paths.find(node)->second.getDistance();
|
||||||
|
}
|
||||||
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
void removeLinkTo(TravelNode* node, bool removePaths = false);
|
||||||
|
|
||||||
bool isEqual(TravelNode* compareNode);
|
bool isEqual(TravelNode* compareNode);
|
||||||
@ -304,7 +363,8 @@ public:
|
|||||||
bool cropUselessLinks();
|
bool cropUselessLinks();
|
||||||
|
|
||||||
// Returns all nodes that can be reached from this node.
|
// Returns all nodes that can be reached from this node.
|
||||||
std::vector<TravelNode*> getNodeMap(bool importantOnly = false, std::vector<TravelNode*> ignoreNodes = {});
|
std::vector<TravelNode*> getNodeMap(bool importantOnly = false,
|
||||||
|
std::vector<TravelNode*> ignoreNodes = {});
|
||||||
|
|
||||||
// Checks if it is even possible to route to this node.
|
// Checks if it is even possible to route to this node.
|
||||||
bool hasRouteTo(TravelNode* node)
|
bool hasRouteTo(TravelNode* node)
|
||||||
@ -314,7 +374,10 @@ public:
|
|||||||
routes[mNode] = true;
|
routes[mNode] = true;
|
||||||
|
|
||||||
return routes.find(node) != routes.end();
|
return routes.find(node) != routes.end();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
void clearRoutes() { routes.clear(); }
|
||||||
|
void setRouteTo(TravelNode* node) { routes[node] = true; }
|
||||||
|
|
||||||
void print(bool printFailed = true);
|
void print(bool printFailed = true);
|
||||||
|
|
||||||
@ -344,63 +407,66 @@ protected:
|
|||||||
// uint32 transportId = 0;
|
// uint32 transportId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PortalNode : public TravelNode
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PortalNode(TravelNode* baseNode) : TravelNode(baseNode){};
|
|
||||||
|
|
||||||
void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell)
|
|
||||||
{
|
|
||||||
nodeName = baseNode->getName();
|
|
||||||
point = *baseNode->getPosition();
|
|
||||||
paths.clear();
|
|
||||||
links.clear();
|
|
||||||
TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true);
|
|
||||||
setPathTo(endNode, path);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route step type
|
// Route step type
|
||||||
enum PathNodeType
|
enum class PathNodeType : uint8
|
||||||
{
|
{
|
||||||
NODE_PREPATH = 0,
|
NODE_PREPATH = 0,
|
||||||
NODE_PATH = 1,
|
NODE_PATH = 1,
|
||||||
NODE_NODE = 2,
|
NODE_NODE = 2,
|
||||||
NODE_PORTAL = 3,
|
NODE_AREA_TRIGGER = 3,
|
||||||
NODE_TRANSPORT = 4,
|
NODE_TRANSPORT = 4,
|
||||||
NODE_FLIGHTPATH = 5,
|
NODE_FLIGHTPATH = 5,
|
||||||
NODE_TELEPORT = 6
|
// value 6 (NODE_TELEPORT) reserved — no consumer; re-add when a
|
||||||
|
// teleport-spell handler / generator returns.
|
||||||
|
NODE_STATIC_PORTAL = 7
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PathNodePoint
|
struct PathNodePoint
|
||||||
{
|
{
|
||||||
WorldPosition point;
|
WorldPosition point;
|
||||||
PathNodeType type = NODE_PATH;
|
PathNodeType type = PathNodeType::NODE_PATH;
|
||||||
uint32 entry = 0;
|
uint32 entry = 0;
|
||||||
|
|
||||||
|
bool operator==(const PathNodePoint& p1) const
|
||||||
|
{
|
||||||
|
return point == p1.point && type == p1.type && entry == p1.entry;
|
||||||
|
}
|
||||||
|
// A "walkable" node is one we traverse on foot. Portals/transports/
|
||||||
|
// taxis/teleports are entry/exit hops, not points to anchor a
|
||||||
|
// shortcut on. Used by makeShortCut to skip them when picking the
|
||||||
|
// closest-point-on-path to the bot.
|
||||||
|
bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// A complete list of points the bots has to walk to or teleport to.
|
// A complete list of points the bots has to walk to or teleport to.
|
||||||
class TravelPath
|
class TravelPath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelPath(){};
|
TravelPath() {}
|
||||||
TravelPath(std::vector<PathNodePoint> fullPath1) { fullPath = fullPath1; }
|
TravelPath(std::vector<PathNodePoint> fullPath1)
|
||||||
TravelPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
{
|
||||||
|
fullPath = fullPath1;
|
||||||
|
}
|
||||||
|
TravelPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
addPath(path, type, entry);
|
addPath(path, type, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
void addPoint(PathNodePoint point) { fullPath.push_back(point); }
|
||||||
void addPoint(WorldPosition point, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPoint(WorldPosition point,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
fullPath.push_back(PathNodePoint{point, type, entry});
|
fullPath.push_back(PathNodePoint{point, type, entry});
|
||||||
}
|
}
|
||||||
void addPath(std::vector<WorldPosition> path, PathNodeType type = NODE_PATH, uint32 entry = 0)
|
void addPath(std::vector<WorldPosition> path,
|
||||||
|
PathNodeType type = PathNodeType::NODE_PATH,
|
||||||
|
uint32 entry = 0)
|
||||||
{
|
{
|
||||||
for (auto& p : path)
|
for (auto& p : path)
|
||||||
{
|
|
||||||
fullPath.push_back(PathNodePoint{p, type, entry});
|
fullPath.push_back(PathNodePoint{p, type, entry});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
void addPath(std::vector<PathNodePoint> newPath)
|
void addPath(std::vector<PathNodePoint> newPath)
|
||||||
{
|
{
|
||||||
@ -408,8 +474,11 @@ public:
|
|||||||
}
|
}
|
||||||
void clear() { fullPath.clear(); }
|
void clear() { fullPath.clear(); }
|
||||||
|
|
||||||
bool empty() { return fullPath.empty(); }
|
bool empty() const { return fullPath.empty(); }
|
||||||
std::vector<PathNodePoint> getPath() { return fullPath; }
|
size_t size() const { return fullPath.size(); }
|
||||||
|
const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; }
|
||||||
|
std::vector<PathNodePoint> GetPath() { return fullPath; }
|
||||||
|
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
|
||||||
WorldPosition getFront() { return fullPath.front().point; }
|
WorldPosition getFront() { return fullPath.front().point; }
|
||||||
WorldPosition getBack() { return fullPath.back().point; }
|
WorldPosition getBack() { return fullPath.back().point; }
|
||||||
|
|
||||||
@ -419,13 +488,14 @@ public:
|
|||||||
for (auto const& p : fullPath)
|
for (auto const& p : fullPath)
|
||||||
retVec.push_back(p.point);
|
retVec.push_back(p.point);
|
||||||
return retVec;
|
return retVec;
|
||||||
};
|
}
|
||||||
|
|
||||||
bool makeShortCut(WorldPosition startPos, float maxDist);
|
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
||||||
bool shouldMoveToNextPoint(WorldPosition startPos, std::vector<PathNodePoint>::iterator beg,
|
|
||||||
std::vector<PathNodePoint>::iterator ed, std::vector<PathNodePoint>::iterator p,
|
// Reject paths the navmesh accepts but a player can't walk:
|
||||||
float& moveDist, float maxDist);
|
// 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1.
|
||||||
WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry);
|
static bool IsPathCheating(std::vector<WorldPosition> const& path,
|
||||||
|
float endpointDistance);
|
||||||
|
|
||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
|
|
||||||
@ -438,16 +508,24 @@ class TravelNodeRoute
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TravelNodeRoute() {}
|
TravelNodeRoute() {}
|
||||||
TravelNodeRoute(std::vector<TravelNode*> nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ }
|
TravelNodeRoute(std::vector<TravelNode*> nodes1)
|
||||||
|
{
|
||||||
|
nodes = nodes1;
|
||||||
|
}
|
||||||
|
|
||||||
bool isEmpty() { return nodes.empty(); }
|
bool isEmpty() { return nodes.empty(); }
|
||||||
|
|
||||||
bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); }
|
bool hasNode(TravelNode* node)
|
||||||
|
{
|
||||||
|
return findNode(node) != nodes.end();
|
||||||
|
}
|
||||||
float getTotalDistance();
|
float getTotalDistance();
|
||||||
|
|
||||||
std::vector<TravelNode*> getNodes() { return nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
|
|
||||||
TravelPath buildPath(std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToEnd = {},
|
TravelPath BuildPath(
|
||||||
|
std::vector<WorldPosition> pathToStart = {},
|
||||||
|
std::vector<WorldPosition> pathToEnd = {},
|
||||||
Unit* bot = nullptr);
|
Unit* bot = nullptr);
|
||||||
|
|
||||||
std::ostringstream const print();
|
std::ostringstream const print();
|
||||||
@ -467,12 +545,43 @@ public:
|
|||||||
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; }
|
||||||
|
|
||||||
TravelNode* dataNode;
|
TravelNode* dataNode;
|
||||||
float m_f = 0.0, m_g = 0.0, m_h = 0.0;
|
float totalCost = 0.0;
|
||||||
bool open = false, close = false;
|
float costFromStart = 0.0;
|
||||||
|
float heuristic = 0.0;
|
||||||
|
bool open = false;
|
||||||
|
bool closed = false;
|
||||||
TravelNodeStub* parent = nullptr;
|
TravelNodeStub* parent = nullptr;
|
||||||
uint32 currentGold = 0;
|
uint32 currentGold = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TravelPlan
|
||||||
|
{
|
||||||
|
WorldPosition destination;
|
||||||
|
|
||||||
|
// Flat waypoint path built upfront by GetFullPath:
|
||||||
|
TravelPath steps;
|
||||||
|
uint32 stepIdx{0};
|
||||||
|
|
||||||
|
// Spline scratch (used by executor):
|
||||||
|
std::vector<G3D::Vector3> walkPoints;
|
||||||
|
uint32 expectedDuration{0}; // used to derive the lastMove delay
|
||||||
|
|
||||||
|
// Taxi scratch:
|
||||||
|
std::vector<uint32> route;
|
||||||
|
|
||||||
|
bool IsActive() const { return !steps.empty(); }
|
||||||
|
|
||||||
|
void Reset()
|
||||||
|
{
|
||||||
|
destination = WorldPosition();
|
||||||
|
steps.clear();
|
||||||
|
stepIdx = 0;
|
||||||
|
walkPoints.clear();
|
||||||
|
expectedDuration = 0;
|
||||||
|
route.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// The container of all nodes.
|
// The container of all nodes.
|
||||||
class TravelNodeMap
|
class TravelNodeMap
|
||||||
{
|
{
|
||||||
@ -484,14 +593,18 @@ public:
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false,
|
TravelNode* addNode(WorldPosition pos,
|
||||||
bool checkDuplicate = true, bool transport = false, uint32 transportId = 0);
|
std::string const preferedName = "Travel Node",
|
||||||
|
bool isImportant = false,
|
||||||
|
bool checkDuplicate = true,
|
||||||
|
bool transport = false,
|
||||||
|
uint32 transportId = 0);
|
||||||
void removeNode(TravelNode* node);
|
void removeNode(TravelNode* node);
|
||||||
bool removeNodes()
|
bool removeNodes()
|
||||||
{
|
{
|
||||||
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
if (m_nMapMtx.try_lock_for(std::chrono::seconds(10)))
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
removeNode(node);
|
removeNode(node);
|
||||||
|
|
||||||
m_nMapMtx.unlock();
|
m_nMapMtx.unlock();
|
||||||
@ -499,28 +612,32 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
void fullLinkNode(TravelNode* startNode, Unit* bot);
|
||||||
|
|
||||||
// Get all nodes
|
// Get all nodes
|
||||||
std::vector<TravelNode*> getNodes() { return m_nodes; }
|
std::vector<TravelNode*> getNodes() { return nodes; }
|
||||||
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
std::vector<TravelNode*> getNodes(WorldPosition pos, float range = -1);
|
||||||
|
|
||||||
// Find nearest node.
|
// Find nearest node.
|
||||||
TravelNode* getNode(TravelNode* sameNode)
|
TravelNode* getNode(TravelNode* sameNode)
|
||||||
{
|
{
|
||||||
for (auto& node : m_nodes)
|
for (auto& node : nodes)
|
||||||
{
|
{
|
||||||
if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition())
|
if (node->getName() == sameNode->getName()
|
||||||
|
&& node->getPosition() == sameNode->getPosition())
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNode* getNode(WorldPosition pos, std::vector<WorldPosition>& ppath, Unit* bot = nullptr, float range = -1);
|
TravelNode* getNode(WorldPosition pos,
|
||||||
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1)
|
std::vector<WorldPosition>& ppath,
|
||||||
|
Unit* bot = nullptr, float range = -1);
|
||||||
|
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
|
||||||
|
float range = -1)
|
||||||
{
|
{
|
||||||
std::vector<WorldPosition> ppath;
|
std::vector<WorldPosition> ppath;
|
||||||
return getNode(pos, ppath, bot, range);
|
return getNode(pos, ppath, bot, range);
|
||||||
@ -536,19 +653,17 @@ public:
|
|||||||
return rNodes[urand(0, rNodes.size() - 1)];
|
return rNodes[urand(0, rNodes.size() - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the best nodePath between two nodes
|
// Finds the best nodePath between two nodes (A* over the node graph)
|
||||||
TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr);
|
TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal,
|
||||||
|
Player* bot);
|
||||||
|
|
||||||
// Find the best node between two positions
|
// Picks the nearest start/end nodes for two world positions and runs A*
|
||||||
TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector<WorldPosition>& startPath,
|
// over the node graph to return a full route between them.
|
||||||
|
TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos,
|
||||||
|
WorldPosition endPos,
|
||||||
|
std::vector<WorldPosition>& startPath,
|
||||||
Player* bot = nullptr);
|
Player* bot = nullptr);
|
||||||
|
|
||||||
// Find the full path between those locations
|
|
||||||
static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr);
|
|
||||||
|
|
||||||
// Manage/update nodes
|
|
||||||
void manageNodes(Unit* bot, bool mapFull = false);
|
|
||||||
|
|
||||||
void setHasToGen() { hasToGen = true; }
|
void setHasToGen() { hasToGen = true; }
|
||||||
|
|
||||||
void generateNpcNodes();
|
void generateNpcNodes();
|
||||||
@ -563,15 +678,17 @@ public:
|
|||||||
void removeUselessPaths();
|
void removeUselessPaths();
|
||||||
void calculatePathCosts();
|
void calculatePathCosts();
|
||||||
void generateTaxiPaths();
|
void generateTaxiPaths();
|
||||||
void generatePaths();
|
void generatePaths(bool fullGen = false);
|
||||||
|
|
||||||
void generateAll();
|
void generateAll();
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
|
||||||
void printMap();
|
void printMap();
|
||||||
|
|
||||||
void printNodeStore();
|
void printNodeStore();
|
||||||
void saveNodeStore();
|
void saveNodeStore();
|
||||||
void loadNodeStore();
|
void LoadNodeStore();
|
||||||
|
|
||||||
bool cropUselessNode(TravelNode* startNode);
|
bool cropUselessNode(TravelNode* startNode);
|
||||||
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
TravelNode* addZoneLinkNode(TravelNode* startNode);
|
||||||
@ -584,8 +701,28 @@ public:
|
|||||||
void InitTaxiGraph();
|
void InitTaxiGraph();
|
||||||
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
|
||||||
|
|
||||||
|
void BuildZoneIndex();
|
||||||
|
void PrecomputeReachability();
|
||||||
|
|
||||||
|
TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId);
|
||||||
|
TravelNode* GetNearestNodeOnMap(WorldPosition pos);
|
||||||
|
|
||||||
|
// All nodes registered to a zone (post-BuildZoneIndex). Returns an
|
||||||
|
// empty static vector for unknown zones.
|
||||||
|
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
|
||||||
|
|
||||||
|
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
|
||||||
|
uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr);
|
||||||
|
|
||||||
|
// Resolve A* route between two world positions (returns node vector)
|
||||||
|
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
|
||||||
|
WorldPosition endPos);
|
||||||
|
|
||||||
|
// Get stored walk points for one edge (from→to). Empty if no path.
|
||||||
|
std::vector<G3D::Vector3> GetEdgeWalkPoints(TravelNode* from,
|
||||||
|
TravelNode* to);
|
||||||
|
|
||||||
std::shared_timed_mutex m_nMapMtx;
|
std::shared_timed_mutex m_nMapMtx;
|
||||||
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TravelNodeMap() = default;
|
TravelNodeMap() = default;
|
||||||
@ -601,13 +738,18 @@ private:
|
|||||||
void BuildTaxiGraph();
|
void BuildTaxiGraph();
|
||||||
void ComputeAllPaths();
|
void ComputeAllPaths();
|
||||||
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
|
||||||
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
|
std::vector<uint32> BuildPath(
|
||||||
|
uint32 fromNode, uint32 toNode,
|
||||||
const std::unordered_map<uint32, uint32>& parentMap);
|
const std::unordered_map<uint32, uint32>& parentMap);
|
||||||
|
|
||||||
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
|
std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
|
||||||
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
|
std::map<uint32, std::map<uint32, std::vector<uint32>>>
|
||||||
|
m_taxiPathCache;
|
||||||
|
|
||||||
std::vector<TravelNode*> m_nodes;
|
std::vector<TravelNode*> nodes;
|
||||||
|
|
||||||
|
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
|
||||||
|
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
|
||||||
|
|
||||||
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,6 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
|
globalCoolDown = sConfigMgr->GetOption<int32>("AiPlayerbot.GlobalCooldown", 500);
|
||||||
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
|
maxWaitForMove = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxWaitForMove", 5000);
|
||||||
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
|
disableMoveSplinePath = sConfigMgr->GetOption<int32>("AiPlayerbot.DisableMoveSplinePath", 0);
|
||||||
maxMovementSearchTime = sConfigMgr->GetOption<int32>("AiPlayerbot.MaxMovementSearchTime", 3);
|
|
||||||
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
|
expireActionTime = sConfigMgr->GetOption<int32>("AiPlayerbot.ExpireActionTime", 5000);
|
||||||
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
|
dispelAuraDuration = sConfigMgr->GetOption<int32>("AiPlayerbot.DispelAuraDuration", 700);
|
||||||
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
|
reactDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReactDelay", 100);
|
||||||
@ -84,8 +83,6 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000);
|
sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000);
|
||||||
returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000);
|
returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000);
|
||||||
lootDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.LootDelay", 1000);
|
lootDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.LootDelay", 1000);
|
||||||
minBotsForGreaterBuff = sConfigMgr->GetOption<int32>("AiPlayerbot.MinBotsForGreaterBuff", 3);
|
|
||||||
rpWarningCooldown = sConfigMgr->GetOption<int32>("AiPlayerbot.RPWarningCooldown", 30);
|
|
||||||
disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30);
|
disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30);
|
||||||
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
|
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
|
||||||
|
|
||||||
@ -116,6 +113,32 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65);
|
highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65);
|
||||||
autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true);
|
autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true);
|
||||||
saveManaThreshold = sConfigMgr->GetOption<int32>("AiPlayerbot.SaveManaThreshold", 60);
|
saveManaThreshold = sConfigMgr->GetOption<int32>("AiPlayerbot.SaveManaThreshold", 60);
|
||||||
|
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoGreaterBlessings", 1))
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
autoGreaterBlessings = AutoPartyBuffMode::DISABLED;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
autoGreaterBlessings = AutoPartyBuffMode::GROUP_OR_RAID;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
autoGreaterBlessings = AutoPartyBuffMode::RAID_ONLY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoPartyBuffs", 2))
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
autoPartyBuffs = AutoPartyBuffMode::DISABLED;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
autoPartyBuffs = AutoPartyBuffMode::RAID_ONLY;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
autoPartyBuffs = AutoPartyBuffMode::GROUP_OR_RAID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
autoAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoAvoidAoe", true);
|
autoAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoAvoidAoe", true);
|
||||||
maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f);
|
maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f);
|
||||||
LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"),
|
LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"),
|
||||||
@ -654,6 +677,7 @@ bool PlayerbotAIConfig::Initialize()
|
|||||||
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
autoTeleportForLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoTeleportForLevel", false);
|
||||||
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
autoDoQuests = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoDoQuests", true);
|
||||||
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
enableNewRpgStrategy = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableNewRpgStrategy", true);
|
||||||
|
enableTravelNodes = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableTravelNodes", false);
|
||||||
|
|
||||||
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15);
|
||||||
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20);
|
||||||
|
|||||||
@ -40,6 +40,13 @@ enum class HealingManaEfficiency : uint8
|
|||||||
SUPERIOR = 32
|
SUPERIOR = 32
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class AutoPartyBuffMode : uint8
|
||||||
|
{
|
||||||
|
DISABLED = 0,
|
||||||
|
RAID_ONLY = 1,
|
||||||
|
GROUP_OR_RAID = 2
|
||||||
|
};
|
||||||
|
|
||||||
enum NewRpgStatus : int
|
enum NewRpgStatus : int
|
||||||
{
|
{
|
||||||
//Initial Status
|
//Initial Status
|
||||||
@ -84,7 +91,7 @@ public:
|
|||||||
bool EnableICCBuffs;
|
bool EnableICCBuffs;
|
||||||
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
|
bool allowAccountBots, allowGuildBots, allowTrustedAccountBots;
|
||||||
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
|
bool randomBotGuildNearby, randomBotInvitePlayer, inviteChat;
|
||||||
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, maxMovementSearchTime, expireActionTime,
|
uint32 globalCoolDown, reactDelay, maxWaitForMove, disableMoveSplinePath, expireActionTime,
|
||||||
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
|
dispelAuraDuration, passiveDelay, repeatDelay, errorDelay, rpgDelay, sitDelay, returnDelay, lootDelay;
|
||||||
bool dynamicReactDelay;
|
bool dynamicReactDelay;
|
||||||
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
|
||||||
@ -94,6 +101,8 @@ public:
|
|||||||
uint32 lowMana, mediumMana, highMana;
|
uint32 lowMana, mediumMana, highMana;
|
||||||
bool autoSaveMana;
|
bool autoSaveMana;
|
||||||
uint32 saveManaThreshold;
|
uint32 saveManaThreshold;
|
||||||
|
AutoPartyBuffMode autoGreaterBlessings;
|
||||||
|
AutoPartyBuffMode autoPartyBuffs;
|
||||||
bool autoAvoidAoe;
|
bool autoAvoidAoe;
|
||||||
float maxAoeAvoidRadius;
|
float maxAoeAvoidRadius;
|
||||||
std::set<uint32> aoeAvoidSpellWhitelist;
|
std::set<uint32> aoeAvoidSpellWhitelist;
|
||||||
@ -146,12 +155,6 @@ public:
|
|||||||
uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay;
|
uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay;
|
||||||
bool randomBotJoinLfg;
|
bool randomBotJoinLfg;
|
||||||
|
|
||||||
// Buff system
|
|
||||||
// Min group size to use Greater buffs (Paladin, Mage, Druid). Default: 3
|
|
||||||
int32 minBotsForGreaterBuff;
|
|
||||||
// Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30
|
|
||||||
int32 rpWarningCooldown;
|
|
||||||
|
|
||||||
// Professions
|
// Professions
|
||||||
bool enableFishingWithMaster;
|
bool enableFishingWithMaster;
|
||||||
uint32 classMatchingProfessionChance;
|
uint32 classMatchingProfessionChance;
|
||||||
@ -372,6 +375,7 @@ public:
|
|||||||
bool autoLearnTrainerSpells;
|
bool autoLearnTrainerSpells;
|
||||||
bool autoDoQuests;
|
bool autoDoQuests;
|
||||||
bool enableNewRpgStrategy;
|
bool enableNewRpgStrategy;
|
||||||
|
bool enableTravelNodes;
|
||||||
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
std::unordered_map<NewRpgStatus, uint32> RpgStatusProbWeight;
|
||||||
bool syncLevelWithPlayers;
|
bool syncLevelWithPlayers;
|
||||||
bool autoLearnQuestSpells;
|
bool autoLearnQuestSpells;
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include "PlayerbotMgr.h"
|
#include "PlayerbotMgr.h"
|
||||||
#include "RandomPlayerbotMgr.h"
|
#include "RandomPlayerbotMgr.h"
|
||||||
#include "ScriptMgr.h"
|
#include "ScriptMgr.h"
|
||||||
|
#include "TravelNode.h"
|
||||||
|
|
||||||
using namespace Acore::ChatCommands;
|
using namespace Acore::ChatCommands;
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ public:
|
|||||||
{
|
{
|
||||||
static ChatCommandTable playerbotsDebugCommandTable = {
|
static ChatCommandTable playerbotsDebugCommandTable = {
|
||||||
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
|
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
{"zone", HandleDebugZoneCommand, SEC_GAMEMASTER, Console::No},
|
||||||
};
|
};
|
||||||
|
|
||||||
static ChatCommandTable playerbotsAccountCommandTable = {
|
static ChatCommandTable playerbotsAccountCommandTable = {
|
||||||
@ -41,11 +43,16 @@ public:
|
|||||||
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static ChatCommandTable playerbotsTravelCommandTable = {
|
||||||
|
{"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
};
|
||||||
|
|
||||||
static ChatCommandTable playerbotsCommandTable = {
|
static ChatCommandTable playerbotsCommandTable = {
|
||||||
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
|
||||||
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
|
||||||
|
{"travel", playerbotsTravelCommandTable},
|
||||||
{"debug", playerbotsDebugCommandTable},
|
{"debug", playerbotsDebugCommandTable},
|
||||||
{"account", playerbotsAccountCommandTable},
|
{"account", playerbotsAccountCommandTable},
|
||||||
};
|
};
|
||||||
@ -106,11 +113,177 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("Regenerating travel node paths...");
|
||||||
|
LOG_INFO("playerbots", "Manual travel node regeneration started via console command.");
|
||||||
|
sTravelNodeMap.generateAll();
|
||||||
|
handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
|
||||||
{
|
{
|
||||||
return BGTactics::HandleConsoleCommand(handler, args);
|
return BGTactics::HandleConsoleCommand(handler, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Visual constants for showpath markers. Two waypoint-family
|
||||||
|
// creatures give nodes vs path waypoints distinct visuals; both
|
||||||
|
// render at their creature_template default scale (no override).
|
||||||
|
// nodes (anchors) → 15897, prominent waypoint variant
|
||||||
|
// path waypoints → 15631, standard BG-showpath waypoint
|
||||||
|
//
|
||||||
|
// SHOWPATH_PATH_DISPLAY_ID = 0 uses the path-creature's default
|
||||||
|
// model. To experiment with a model override, set this to a known-
|
||||||
|
// good creature display ID for your DB (spell-visual IDs are not
|
||||||
|
// universally registered as creature displays — using one risks
|
||||||
|
// summoning invisible markers).
|
||||||
|
static constexpr uint32 SHOWPATH_NODE_CREATURE = 15897;
|
||||||
|
static constexpr uint32 SHOWPATH_PATH_CREATURE = 15631;
|
||||||
|
static constexpr uint32 SHOWPATH_PATH_DISPLAY_ID = 0; // 0 = default model
|
||||||
|
static constexpr uint32 SHOWPATH_DESPAWN_MS = 60000;
|
||||||
|
|
||||||
|
static bool HandleDebugZoneCommand(ChatHandler* handler, char const* args)
|
||||||
|
{
|
||||||
|
Player* player = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
|
||||||
|
if (!player)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("Command requires an in-game player.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args || !*args)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* cmd = strtok(const_cast<char*>(args), " ");
|
||||||
|
// showpath=all → nodes + cached path waypoints (full picture)
|
||||||
|
// showpath=node → only node anchors
|
||||||
|
// showpath=path → only cached path waypoints (no anchors)
|
||||||
|
bool showNodes = false;
|
||||||
|
bool showLinks = false;
|
||||||
|
if (cmd && strcmp(cmd, "showpath=all") == 0)
|
||||||
|
{
|
||||||
|
showNodes = true;
|
||||||
|
showLinks = true;
|
||||||
|
}
|
||||||
|
else if (cmd && strcmp(cmd, "showpath=node") == 0)
|
||||||
|
{
|
||||||
|
showNodes = true;
|
||||||
|
showLinks = false;
|
||||||
|
}
|
||||||
|
else if (cmd && strcmp(cmd, "showpath=path") == 0)
|
||||||
|
{
|
||||||
|
showNodes = false;
|
||||||
|
showLinks = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("usage: .playerbots debug zone showpath=all|node|path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 zoneId = player->GetZoneId();
|
||||||
|
std::vector<TravelNode*> const& nodes = sTravelNodeMap.GetNodesInZone(zoneId);
|
||||||
|
if (nodes.empty())
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("No travel nodes registered in zone {} (is the travel node system loaded?)", zoneId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// node markers — full-scale anchor at each travel-node position.
|
||||||
|
uint32 nodesPlaced = 0;
|
||||||
|
if (showNodes)
|
||||||
|
{
|
||||||
|
for (TravelNode* node : nodes)
|
||||||
|
{
|
||||||
|
if (!node)
|
||||||
|
continue;
|
||||||
|
WorldPosition* pos = node->getPosition();
|
||||||
|
if (!pos || pos->GetMapId() != player->GetMapId())
|
||||||
|
continue;
|
||||||
|
Creature* wp = player->SummonCreature(SHOWPATH_NODE_CREATURE,
|
||||||
|
pos->GetPositionX(), pos->GetPositionY(),
|
||||||
|
pos->GetPositionZ(), 0,
|
||||||
|
TEMPSUMMON_TIMED_DESPAWN, SHOWPATH_DESPAWN_MS);
|
||||||
|
if (wp)
|
||||||
|
{
|
||||||
|
wp->SetOwnerGUID(player->GetGUID());
|
||||||
|
++nodesPlaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showLinks)
|
||||||
|
{
|
||||||
|
handler->PSendSysMessage("Showing {} travel nodes in zone {} (60s)", nodesPlaced, zoneId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// path-waypoint markers — same creature, scaled down so they
|
||||||
|
// read as a breadcrumb trail between nodes rather than as more
|
||||||
|
// anchor points. Walk-type links from any in-zone node are
|
||||||
|
// drawn; the per-waypoint same-map filter keeps the trail from
|
||||||
|
// running into other continents. Sparse zones (e.g. Teldrassil)
|
||||||
|
// would draw nothing if we required dst-in-zone too, since their
|
||||||
|
// only links go to nodes in neighbouring zones.
|
||||||
|
constexpr uint32 MAX_PATH_MARKERS = 500;
|
||||||
|
uint32 pathPlaced = 0;
|
||||||
|
uint32 linksDrawn = 0;
|
||||||
|
bool capped = false;
|
||||||
|
for (TravelNode* node : nodes)
|
||||||
|
{
|
||||||
|
if (!node)
|
||||||
|
continue;
|
||||||
|
auto* links = node->getLinks();
|
||||||
|
if (!links)
|
||||||
|
continue;
|
||||||
|
for (auto const& kv : *links)
|
||||||
|
{
|
||||||
|
TravelNode* dst = kv.first;
|
||||||
|
TravelNodePath* path = kv.second;
|
||||||
|
if (!dst || !path)
|
||||||
|
continue;
|
||||||
|
if (path->getPathType() != TravelNodePathType::walk)
|
||||||
|
continue;
|
||||||
|
++linksDrawn;
|
||||||
|
for (WorldPosition const& wpPos : path->GetPath())
|
||||||
|
{
|
||||||
|
if (wpPos.GetMapId() != player->GetMapId())
|
||||||
|
continue;
|
||||||
|
if (pathPlaced >= MAX_PATH_MARKERS)
|
||||||
|
{
|
||||||
|
capped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Creature* mk = player->SummonCreature(SHOWPATH_PATH_CREATURE,
|
||||||
|
wpPos.GetPositionX(),
|
||||||
|
wpPos.GetPositionY(), wpPos.GetPositionZ(),
|
||||||
|
0, TEMPSUMMON_TIMED_DESPAWN,
|
||||||
|
SHOWPATH_DESPAWN_MS);
|
||||||
|
if (mk)
|
||||||
|
{
|
||||||
|
mk->SetOwnerGUID(player->GetGUID());
|
||||||
|
if (SHOWPATH_PATH_DISPLAY_ID)
|
||||||
|
mk->SetDisplayId(SHOWPATH_PATH_DISPLAY_ID);
|
||||||
|
++pathPlaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (capped)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (capped)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler->PSendSysMessage("Showing {} nodes + {} path waypoints across {} walk links in zone {}{} (60s)",
|
||||||
|
nodesPlaced, pathPlaced, linksDrawn, zoneId,
|
||||||
|
capped ? " — capped at 500 path markers" : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
|
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
|
||||||
{
|
{
|
||||||
if (!args || !*args)
|
if (!args || !*args)
|
||||||
|
|||||||
24
todo.md
Normal file
24
todo.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Aligned:
|
||||||
|
|
||||||
|
Bot 50° cap + water 10× bias at engine filter level (covers every path query)
|
||||||
|
PathFinder reuse + Clear() per step
|
||||||
|
Underwater path-extension + dispatch fixup
|
||||||
|
GetFullPath mmap-probe-first
|
||||||
|
BG gating
|
||||||
|
ClipPath (LOS + level+5)
|
||||||
|
Inactive-bot teleport (with self-bot carve-out — intentional)
|
||||||
|
masterWalking
|
||||||
|
Pre-dispatch state cleanup
|
||||||
|
setPath before mutations
|
||||||
|
needsLongPath + cross-map gate
|
||||||
|
StopMoving short-stop
|
||||||
|
10% reuse + regression guard
|
||||||
|
WaitForReach formula
|
||||||
|
Stateless re-resolve
|
||||||
|
walkDistance config
|
||||||
|
Differ (blocked on infrastructure):
|
||||||
|
|
||||||
|
Hazard avoidance (GeneratePathAvoidingHazards) — needs hazard system
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
Flying / transports / vehicles
|
||||||
Loading…
x
Reference in New Issue
Block a user