feat(Core/Playerbots): Initialize bot professions and specializations (#2287)

## Pull Request Description

Initialize random bot professions from the factory using class-matching
or weighted-random profession pairs, respect the active primary
profession cap, and restore required profession tools during bot
init/refresh.

This PR also initializes profession specializations for eligible bots so
crafted professions are not left in an unspecialized state after
profession assignment. Supported specialization families include:

- Alchemy: Transmute / Elixir / Potion
- Engineering: Goblin / Gnomish
- Leatherworking: Dragonscale / Elemental / Tribal
- Tailoring: Spellfire / Mooncloth / Shadoweave
- Blacksmithing: Armorsmith / Weaponsmith, plus Hammersmith / Axesmith /
Swordsmith for eligible Weaponsmith bots

Specialization choices are stored in bot values so they remain stable
across later refreshes. Required tool items are also restored for
relevant professions during maintenance.

## Feature Evaluation

- Describe the **minimum logic** required to achieve the intended
behavior.
- Select one or two professions during factory initialization from a
small weighted list.
- Clamp the assigned professions to the configured primary profession
limit.
- Learn the profession starter spell and set skill to the bot’s
profession cap.
- For professions with supported specialization branches, assign exactly
one valid specialization when the bot meets the same level/skill gates
used by AzerothCore profession scripts.
- Persist the specialization selection in stored bot values so the
choice is stable and does not need to be recalculated repeatedly.
- Restore missing profession tools only when the bot has the related
profession and the tool is absent.

- Describe the **processing cost** when this logic executes across many
bots.
- The added logic executes only during bot init/refresh, not as part of
per-tick combat or trigger evaluation.
- Runtime cost is limited to a few small switch statements, stored value
lookups, spell checks, and item presence checks.
- No expensive repeated searches, map scans, or per-trigger decision
trees were added.
- The design keeps specialization selection deterministic after first
assignment by storing the result, avoiding repeated random branching
later.

## How to Test the Changes

1. Build and restart the server with this branch.
2. Trigger random bot creation, refresh, or level-based reroll for
multiple bots.
3. Verify in `Playerbots.log` that bots receive profession pairs and,
when eligible, profession specializations.
4. Check that low-level bots do not receive specializations before the
required thresholds.
5. Check that eligible bots do receive one specialization for supported
profession families.
6. Verify that specialization choices remain stable across subsequent
refreshes.
7. Verify that profession tools are restored when missing:
   - Mining Pick
   - Blacksmith Hammer
   - Arclight Spanner
   - Runed Arcanite Rod
   - Skinning Knife
8. For a few bots, inspect in game or via debug tooling that profession
spells/specialization spells are present as expected.

Expected behavior:
- Bots receive professions that respect the configured primary
profession limit.
- Profession skill values are initialized to the level-based cap.
- Eligible bots receive exactly one valid specialization for supported
profession families.
- Specialization assignments are logged and persist across refreshes.
- Profession tools are restored only when required.

## Impact Assessment

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

  Explanation:
- The added work runs during initialization/refresh rather than normal
per-tick behavior.
- Logic is bounded, data-local, and based on direct skill/spell/value
checks.

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

  Explanation:
- Bots can now start with initialized professions, required tools, and
eligible profession specializations instead of remaining partially
configured or unspecialized.

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

  Explanation:
- The factory now contains specialization assignment branches for
supported profession families.
- Complexity is intentionally limited to init-time switch-based logic
with stored specialization values to preserve predictability.

## AI Assistance

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

AI assistance was used for:
- code generation and refactoring in `PlayerbotFactory`
- drafting and refining profession/specialization initialization logic
- PR description preparation

All generated and suggested code was reviewed, adjusted, built locally,
and validated before submission.

## Final Checklist

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

## Notes for Reviewers

- Target branch is `test-staging`.
- Profession/specialization logic is intentionally limited to
init/refresh paths to avoid per-tick cost.
- Specialization selections are stored to keep bot behavior stable
across later refreshes.
- Recent changes also add debug logging for assigned specializations and
save the bot after specialization learning so assignments are visible
and persisted.

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
This commit is contained in:
Scarecr0w12 2026-04-17 15:54:36 -05:00 committed by GitHub
parent 6c517eb9d1
commit c0c2b6ab5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 620 additions and 67 deletions

View File

@ -651,9 +651,14 @@ AiPlayerbot.BotTaxiGapJitterMs = 100
####################################################################################################
# PROFESSIONS
# Note: Random bots currently do not get professions
#
# Percentage of randombots in each class bucket that receive a class-matching
# weighted profession combination. The remaining randombots use the weighted
# random sane-pair profession pool.
# Default: 30
AiPlayerbot.ClassMatchingProfessionChance = 30
# Automatically adds the 'master fishing' strategy to bots that have the fishing skill when the bots master fishes.
# Default: 1 (Enabled)
AiPlayerbot.EnableFishingWithMaster = 1

View File

@ -5,6 +5,7 @@
#include "PlayerbotFactory.h"
#include <array>
#include <utility>
#include "AccountMgr.h"
@ -47,10 +48,11 @@ static std::vector<uint32> initSlotsOrder = {EQUIPMENT_SLOT_TRINKET1, EQUIPMENT_
EQUIPMENT_SLOT_LEGS, EQUIPMENT_SLOT_HANDS, EQUIPMENT_SLOT_NECK, EQUIPMENT_SLOT_BODY, EQUIPMENT_SLOT_WAIST,
EQUIPMENT_SLOT_FEET, EQUIPMENT_SLOT_WRISTS, EQUIPMENT_SLOT_FINGER1, EQUIPMENT_SLOT_FINGER2, EQUIPMENT_SLOT_BACK};
uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING, SKILL_TAILORING,
SKILL_LEATHERWORKING, SKILL_ENGINEERING, SKILL_HERBALISM, SKILL_MINING,
SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID, SKILL_FISHING,
SKILL_JEWELCRAFTING};
uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING,
SKILL_TAILORING, SKILL_LEATHERWORKING, SKILL_ENGINEERING,
SKILL_HERBALISM, SKILL_INSCRIPTION, SKILL_MINING,
SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID,
SKILL_FISHING, SKILL_JEWELCRAFTING};
std::list<uint32> PlayerbotFactory::classQuestIds;
std::list<uint32> PlayerbotFactory::specialQuestIds;
@ -58,6 +60,264 @@ std::vector<uint32> PlayerbotFactory::enchantSpellIdCache;
std::vector<uint32> PlayerbotFactory::enchantGemIdCache;
std::unordered_map<uint32, std::vector<uint32>> PlayerbotFactory::trainerIdCache;
bool PlayerbotFactory::IsPrimaryTradeSkill(uint16 skillId)
{
SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(skillId);
return skillLine && skillLine->categoryId == SKILL_CATEGORY_PROFESSION;
}
bool PlayerbotFactory::IsGatheringTradeSkill(uint16 skillId)
{
switch (skillId)
{
case SKILL_HERBALISM:
case SKILL_MINING:
case SKILL_SKINNING:
return true;
default:
return false;
}
}
bool PlayerbotFactory::IsCraftingTradeSkill(uint16 skillId)
{
return IsPrimaryTradeSkill(skillId) && !IsGatheringTradeSkill(skillId);
}
uint32 PlayerbotFactory::GetProfessionStarterSpell(uint16 skillId)
{
static constexpr std::array<std::pair<uint16, uint32>, 14> ProfessionStarterSpells = {{
{SKILL_ALCHEMY, 2259},
{SKILL_BLACKSMITHING, 2018},
{SKILL_COOKING, 2550},
{SKILL_ENCHANTING, 7411},
{SKILL_ENGINEERING, 4036},
{SKILL_FIRST_AID, 3273},
{SKILL_FISHING, 7620},
{SKILL_HERBALISM, 2366},
{SKILL_INSCRIPTION, 45357},
{SKILL_JEWELCRAFTING, 25229},
{SKILL_LEATHERWORKING, 2108},
{SKILL_MINING, 2575},
{SKILL_SKINNING, 8613},
{SKILL_TAILORING, 3908}
}};
for (auto const& [professionSkill, starterSpell] : ProfessionStarterSpells)
{
if (professionSkill == skillId)
return starterSpell;
}
return 0;
}
std::vector<PlayerbotFactory::WeightedProfessionPair> PlayerbotFactory::GetClassProfessionPairs(Player* bot)
{
switch (bot->getClass())
{
case CLASS_WARRIOR:
return {{SKILL_MINING, SKILL_BLACKSMITHING, 45},
{SKILL_MINING, SKILL_ENGINEERING, 30},
{SKILL_MINING, SKILL_JEWELCRAFTING, 15},
{SKILL_HERBALISM, SKILL_ALCHEMY, 10}};
case CLASS_PALADIN:
return {{SKILL_MINING, SKILL_BLACKSMITHING, 45},
{SKILL_MINING, SKILL_JEWELCRAFTING, 30},
{SKILL_MINING, SKILL_ENGINEERING, 15},
{SKILL_HERBALISM, SKILL_ALCHEMY, 10}};
case CLASS_DEATH_KNIGHT:
return {{SKILL_MINING, SKILL_BLACKSMITHING, 45},
{SKILL_MINING, SKILL_ENGINEERING, 35},
{SKILL_MINING, SKILL_JEWELCRAFTING, 20}};
case CLASS_HUNTER:
return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 45},
{SKILL_MINING, SKILL_ENGINEERING, 35},
{SKILL_HERBALISM, SKILL_ALCHEMY, 10},
{SKILL_MINING, SKILL_JEWELCRAFTING, 10}};
case CLASS_ROGUE:
return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35},
{SKILL_HERBALISM, SKILL_ALCHEMY, 25},
{SKILL_MINING, SKILL_ENGINEERING, 25},
{SKILL_MINING, SKILL_JEWELCRAFTING, 10},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 5}};
case CLASS_DRUID:
return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35},
{SKILL_HERBALISM, SKILL_ALCHEMY, 35},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 20},
{SKILL_MINING, SKILL_JEWELCRAFTING, 10}};
case CLASS_SHAMAN:
return {{SKILL_HERBALISM, SKILL_ALCHEMY, 35},
{SKILL_SKINNING, SKILL_LEATHERWORKING, 25},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 25},
{SKILL_MINING, SKILL_JEWELCRAFTING, 15}};
case CLASS_PRIEST:
return {{SKILL_TAILORING, SKILL_ENCHANTING, 45},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 30},
{SKILL_HERBALISM, SKILL_ALCHEMY, 25}};
case CLASS_MAGE:
return {{SKILL_TAILORING, SKILL_ENCHANTING, 50},
{SKILL_HERBALISM, SKILL_ALCHEMY, 25},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 25}};
case CLASS_WARLOCK:
default:
return {{SKILL_TAILORING, SKILL_ENCHANTING, 50},
{SKILL_HERBALISM, SKILL_ALCHEMY, 25},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 25}};
}
}
std::vector<PlayerbotFactory::WeightedProfessionPair> PlayerbotFactory::GetRandomProfessionPairs()
{
return {{SKILL_MINING, SKILL_BLACKSMITHING, 20},
{SKILL_MINING, SKILL_ENGINEERING, 18},
{SKILL_MINING, SKILL_JEWELCRAFTING, 16},
{SKILL_SKINNING, SKILL_LEATHERWORKING, 18},
{SKILL_HERBALISM, SKILL_ALCHEMY, 18},
{SKILL_HERBALISM, SKILL_INSCRIPTION, 14},
{SKILL_TAILORING, SKILL_ENCHANTING, 10},
{SKILL_HERBALISM, SKILL_MINING, 6},
{SKILL_HERBALISM, SKILL_SKINNING, 5},
{SKILL_MINING, SKILL_SKINNING, 5}};
}
std::pair<uint16, uint16> PlayerbotFactory::ChooseProfessionPair(
std::vector<WeightedProfessionPair> const& professionPairs)
{
uint32 totalWeight = 0;
for (WeightedProfessionPair const& pair : professionPairs)
totalWeight += pair.weight;
if (!totalWeight)
return {SKILL_HERBALISM, SKILL_ALCHEMY};
uint32 roll = urand(1, totalWeight);
for (WeightedProfessionPair const& pair : professionPairs)
{
if (roll <= pair.weight)
return {pair.firstSkill, pair.secondSkill};
roll -= pair.weight;
}
WeightedProfessionPair const& fallback = professionPairs.back();
return {fallback.firstSkill, fallback.secondSkill};
}
bool PlayerbotFactory::HasProfessionPair(std::vector<WeightedProfessionPair> const& professionPairs,
uint16 firstSkill, uint16 secondSkill)
{
for (WeightedProfessionPair const& pair : professionPairs)
{
if (pair.firstSkill == firstSkill && pair.secondSkill == secondSkill)
return true;
}
return false;
}
uint16 PlayerbotFactory::ChooseSingleProfession(std::vector<WeightedProfessionPair> const& professionPairs)
{
std::vector<std::pair<uint16, uint32>> gatheringSkills;
std::vector<std::pair<uint16, uint32>> craftingSkills;
auto addWeightedSkill = [](std::vector<std::pair<uint16, uint32>>& skills, uint16 skillId, uint32 weight)
{
for (std::pair<uint16, uint32>& skill : skills)
{
if (skill.first == skillId)
{
skill.second += weight;
return;
}
}
skills.push_back({skillId, weight});
};
for (WeightedProfessionPair const& pair : professionPairs)
{
if (IsGatheringTradeSkill(pair.firstSkill))
addWeightedSkill(gatheringSkills, pair.firstSkill, pair.weight);
if (IsCraftingTradeSkill(pair.firstSkill))
addWeightedSkill(craftingSkills, pair.firstSkill, pair.weight);
if (IsGatheringTradeSkill(pair.secondSkill))
addWeightedSkill(gatheringSkills, pair.secondSkill, pair.weight);
if (IsCraftingTradeSkill(pair.secondSkill))
addWeightedSkill(craftingSkills, pair.secondSkill, pair.weight);
}
std::vector<std::pair<uint16, uint32>>* selectedPool = nullptr;
if (!gatheringSkills.empty() && !craftingSkills.empty())
selectedPool = urand(0, 1) == 0 ? &gatheringSkills : &craftingSkills;
else if (!gatheringSkills.empty())
selectedPool = &gatheringSkills;
else if (!craftingSkills.empty())
selectedPool = &craftingSkills;
if (!selectedPool || selectedPool->empty())
return SKILL_HERBALISM;
uint32 totalWeight = 0;
for (std::pair<uint16, uint32> const& skill : *selectedPool)
totalWeight += skill.second;
if (!totalWeight)
return selectedPool->front().first;
uint32 roll = urand(1, totalWeight);
for (std::pair<uint16, uint32> const& skill : *selectedPool)
{
if (roll <= skill.second)
return skill.first;
roll -= skill.second;
}
return selectedPool->back().first;
}
uint32 PlayerbotFactory::GetStoredOrRandomValue(Player* bot,
std::string const& key,
uint32 minValue,
uint32 maxValue)
{
uint32 value = sRandomPlayerbotMgr.GetValue(bot, key);
if (value < minValue || value > maxValue)
{
value = urand(minValue, maxValue);
sRandomPlayerbotMgr.SetValue(bot, key, value);
}
return value;
}
bool PlayerbotFactory::HasAnySpell(Player* bot, std::vector<uint32> const& spells)
{
for (uint32 spellId : spells)
{
if (bot->HasSpell(spellId))
return true;
}
return false;
}
bool PlayerbotFactory::LearnProfessionSpecialization(Player* bot,
ProfessionSpecializationSpell knownSpell,
ProfessionSpecializationSpell learnSpell)
{
uint32 const knownSpellId = static_cast<uint32>(knownSpell);
uint32 const learnSpellId = static_cast<uint32>(learnSpell);
if (bot->HasSpell(knownSpellId) || !sSpellMgr->GetSpellInfo(learnSpellId))
return false;
bot->CastSpell(bot, learnSpellId, true);
return bot->HasSpell(knownSpellId);
}
PlayerbotFactory::PlayerbotFactory(Player* bot, uint32 level, uint32 itemQuality, uint32 gearScoreLimit)
: level(level), itemQuality(itemQuality), gearScoreLimit(gearScoreLimit), bot(bot)
{
@ -2250,69 +2510,278 @@ bool PlayerbotFactory::CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item)
void PlayerbotFactory::InitTradeSkills()
{
if (!sRandomPlayerbotMgr.IsRandomBot(bot))
return;
uint32 const maxPrimaryTradeSkills =
std::min<uint32>(2, sWorld->getIntConfig(CONFIG_MAX_PRIMARY_TRADE_SKILL));
uint16 firstSkill = sRandomPlayerbotMgr.GetValue(bot, "firstSkill");
uint16 secondSkill = sRandomPlayerbotMgr.GetValue(bot, "secondSkill");
if (!firstSkill || !secondSkill)
{
std::vector<uint32> firstSkills;
std::vector<uint32> secondSkills;
ProfessionRollType professionRollType =
static_cast<ProfessionRollType>(sRandomPlayerbotMgr.GetValue(bot, "professionRollType"));
switch (bot->getClass())
if (professionRollType != ProfessionRollType::Class && professionRollType != ProfessionRollType::Random)
{
case CLASS_WARRIOR:
case CLASS_PALADIN:
case CLASS_DEATH_KNIGHT:
firstSkills.push_back(SKILL_MINING);
secondSkills.push_back(SKILL_BLACKSMITHING);
secondSkills.push_back(SKILL_ENGINEERING);
secondSkills.push_back(SKILL_JEWELCRAFTING);
break;
case CLASS_SHAMAN:
case CLASS_DRUID:
case CLASS_HUNTER:
case CLASS_ROGUE:
firstSkills.push_back(SKILL_SKINNING);
secondSkills.push_back(SKILL_LEATHERWORKING);
break;
default:
firstSkills.push_back(SKILL_TAILORING);
secondSkills.push_back(SKILL_ENCHANTING);
professionRollType = urand(1, 100) <= sPlayerbotAIConfig.classMatchingProfessionChance
? ProfessionRollType::Class
: ProfessionRollType::Random;
sRandomPlayerbotMgr.SetValue(bot, "professionRollType", static_cast<uint32>(professionRollType));
}
switch (urand(0, 6))
std::vector<WeightedProfessionPair> professionPairs = professionRollType == ProfessionRollType::Class
? GetClassProfessionPairs(bot)
: GetRandomProfessionPairs();
bool const hasStoredProfessionPair = firstSkill && secondSkill && firstSkill != secondSkill &&
IsPrimaryTradeSkill(firstSkill) && IsPrimaryTradeSkill(secondSkill) &&
HasProfessionPair(professionPairs, firstSkill, secondSkill);
bool const keepExistingProfessionPair = maxPrimaryTradeSkills < 2 && hasStoredProfessionPair;
if (maxPrimaryTradeSkills == 1 && !keepExistingProfessionPair)
{
case 0:
firstSkill = SKILL_HERBALISM;
secondSkill = SKILL_ALCHEMY;
break;
case 1:
firstSkill = SKILL_HERBALISM;
secondSkill = SKILL_MINING;
break;
case 2:
firstSkill = SKILL_MINING;
secondSkill = SKILL_SKINNING;
break;
case 3:
firstSkill = SKILL_HERBALISM;
secondSkill = SKILL_SKINNING;
break;
default:
firstSkill = firstSkills[urand(0, firstSkills.size() - 1)];
secondSkill = secondSkills[urand(0, secondSkills.size() - 1)];
break;
if (!IsPrimaryTradeSkill(firstSkill) || secondSkill != 0)
{
firstSkill = ChooseSingleProfession(professionPairs);
secondSkill = 0;
sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill);
sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill);
}
}
else if (maxPrimaryTradeSkills == 0 && !keepExistingProfessionPair)
{
firstSkill = 0;
secondSkill = 0;
sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill);
sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill);
}
if (maxPrimaryTradeSkills >= 2 &&
(!firstSkill || !secondSkill || firstSkill == secondSkill || !IsPrimaryTradeSkill(firstSkill) ||
!IsPrimaryTradeSkill(secondSkill) || !HasProfessionPair(professionPairs, firstSkill, secondSkill)))
{
auto const& professionPair = ChooseProfessionPair(professionPairs);
firstSkill = professionPair.first;
secondSkill = professionPair.second;
sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill);
sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill);
}
std::vector<uint16> primarySkills;
if (keepExistingProfessionPair)
{
primarySkills.push_back(firstSkill);
primarySkills.push_back(secondSkill);
}
else if (maxPrimaryTradeSkills > 0)
primarySkills.push_back(firstSkill);
if (!keepExistingProfessionPair && maxPrimaryTradeSkills > 1)
primarySkills.push_back(secondSkill);
SetRandomSkill(SKILL_FIRST_AID);
SetRandomSkill(SKILL_FISHING);
SetRandomSkill(SKILL_COOKING);
SetRandomSkill(firstSkill);
SetRandomSkill(secondSkill);
for (uint16 skillId : primarySkills)
SetRandomSkill(skillId);
std::vector<uint16> skillsToLearn = {SKILL_FIRST_AID, SKILL_FISHING, SKILL_COOKING};
skillsToLearn.insert(skillsToLearn.end(), primarySkills.begin(), primarySkills.end());
for (uint16 skillId : skillsToLearn)
{
uint32 spellId = GetProfessionStarterSpell(skillId);
if (!spellId || bot->HasSpell(spellId))
continue;
if (IsPrimaryTradeSkill(skillId) && !bot->GetFreePrimaryProfessionPoints() &&
!(keepExistingProfessionPair && bot->HasSkill(skillId)))
continue;
bot->learnSpell(spellId, false);
}
InitTradeSpecializations();
}
void PlayerbotFactory::InitTradeSpecializations()
{
InitAlchemySpecialization();
InitEngineeringSpecialization();
InitLeatherworkingSpecialization();
InitTailoringSpecialization();
InitBlacksmithingSpecialization();
}
bool PlayerbotFactory::InitAlchemySpecialization()
{
if (!bot->HasSkill(SKILL_ALCHEMY) ||
bot->GetBaseSkillValue(SKILL_ALCHEMY) < 325 ||
bot->GetLevel() <= 67)
return false;
if (HasAnySpell(bot, {static_cast<uint32>(ProfessionSpecializationSpell::Transmute),
static_cast<uint32>(ProfessionSpecializationSpell::Elixir),
static_cast<uint32>(ProfessionSpecializationSpell::Potion)}))
return false;
switch (GetStoredOrRandomValue(bot, "alchemySpecialization", 1, 3))
{
case 1:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Transmute,
ProfessionSpecializationSpell::LearnTransmute);
case 2:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Elixir,
ProfessionSpecializationSpell::LearnElixir);
case 3:
default:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Potion,
ProfessionSpecializationSpell::LearnPotion);
}
}
bool PlayerbotFactory::InitEngineeringSpecialization()
{
if (!bot->HasSkill(SKILL_ENGINEERING) ||
bot->GetBaseSkillValue(SKILL_ENGINEERING) < 200 ||
bot->GetLevel() < 30)
return false;
if (HasAnySpell(bot, {static_cast<uint32>(ProfessionSpecializationSpell::Goblin),
static_cast<uint32>(ProfessionSpecializationSpell::Gnomish)}))
return false;
switch (GetStoredOrRandomValue(bot, "engineeringSpecialization", 1, 2))
{
case 1:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Goblin,
ProfessionSpecializationSpell::LearnGoblin);
case 2:
default:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Gnomish,
ProfessionSpecializationSpell::LearnGnomish);
}
}
bool PlayerbotFactory::InitLeatherworkingSpecialization()
{
if (!bot->HasSkill(SKILL_LEATHERWORKING) ||
bot->GetBaseSkillValue(SKILL_LEATHERWORKING) < 225 ||
bot->GetLevel() <= 40)
return false;
if (HasAnySpell(bot, {static_cast<uint32>(ProfessionSpecializationSpell::Dragon),
static_cast<uint32>(ProfessionSpecializationSpell::Elemental),
static_cast<uint32>(ProfessionSpecializationSpell::Tribal)}))
return false;
switch (GetStoredOrRandomValue(bot, "leatherSpecialization", 1, 3))
{
case 1:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Dragon,
ProfessionSpecializationSpell::LearnDragon);
case 2:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Elemental,
ProfessionSpecializationSpell::LearnElemental);
case 3:
default:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Tribal,
ProfessionSpecializationSpell::LearnTribal);
}
}
bool PlayerbotFactory::InitTailoringSpecialization()
{
if (!bot->HasSkill(SKILL_TAILORING) ||
bot->GetBaseSkillValue(SKILL_TAILORING) < 350 ||
bot->GetLevel() <= 59)
return false;
if (HasAnySpell(bot, {static_cast<uint32>(ProfessionSpecializationSpell::Spellfire),
static_cast<uint32>(ProfessionSpecializationSpell::Mooncloth),
static_cast<uint32>(ProfessionSpecializationSpell::Shadoweave)}))
return false;
switch (GetStoredOrRandomValue(bot, "tailorSpecialization", 1, 3))
{
case 1:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Spellfire,
ProfessionSpecializationSpell::LearnSpellfire);
case 2:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Mooncloth,
ProfessionSpecializationSpell::LearnMooncloth);
case 3:
default:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Shadoweave,
ProfessionSpecializationSpell::LearnShadoweave);
}
}
bool PlayerbotFactory::InitBlacksmithingSpecialization()
{
bool learnedSpecialization = false;
if (!bot->HasSkill(SKILL_BLACKSMITHING) ||
bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 225)
return false;
if (!bot->HasSpell(static_cast<uint32>(ProfessionSpecializationSpell::Armor)) &&
!bot->HasSpell(static_cast<uint32>(ProfessionSpecializationSpell::Weapon)))
{
switch (GetStoredOrRandomValue(bot, "blacksmithSpecialization", 1, 2))
{
case 1:
learnedSpecialization = LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Armor,
ProfessionSpecializationSpell::LearnArmor);
break;
case 2:
default:
learnedSpecialization = LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Weapon,
ProfessionSpecializationSpell::LearnWeapon);
break;
}
}
if (!bot->HasSpell(static_cast<uint32>(ProfessionSpecializationSpell::Weapon)) ||
bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 250 ||
bot->GetLevel() <= 49 ||
HasAnySpell(bot, {static_cast<uint32>(ProfessionSpecializationSpell::Hammer),
static_cast<uint32>(ProfessionSpecializationSpell::Axe),
static_cast<uint32>(ProfessionSpecializationSpell::Sword)}))
return learnedSpecialization;
switch (GetStoredOrRandomValue(bot, "blacksmithWeaponSpecialization", 1, 3))
{
case 1:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Hammer,
ProfessionSpecializationSpell::LearnHammer);
case 2:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Axe,
ProfessionSpecializationSpell::LearnAxe);
case 3:
default:
return LearnProfessionSpecialization(bot,
ProfessionSpecializationSpell::Sword,
ProfessionSpecializationSpell::LearnSword);
}
}
void PlayerbotFactory::UpdateTradeSkills()
@ -2456,6 +2925,9 @@ void PlayerbotFactory::InitSkills()
break;
}
InitTradeSkills();
InitInventorySkill();
// switch (bot->getClass())
// {
// case CLASS_WARRIOR:
@ -3804,31 +4276,22 @@ void PlayerbotFactory::InitInventory()
void PlayerbotFactory::InitInventorySkill()
{
if (bot->HasSkill(SKILL_MINING))
{
if (bot->HasSkill(SKILL_MINING) && !bot->HasItemCount(2901, 1, true))
StoreItem(2901, 1); // Mining Pick
}
if (bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING))
{
if ((bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING)) &&
!bot->HasItemCount(5956, 1, true))
StoreItem(5956, 1); // Blacksmith Hammer
}
if (bot->HasSkill(SKILL_ENGINEERING))
{
if (bot->HasSkill(SKILL_ENGINEERING) && !bot->HasItemCount(6219, 1, true))
StoreItem(6219, 1); // Arclight Spanner
}
if (bot->HasSkill(SKILL_ENCHANTING))
{
if (bot->HasSkill(SKILL_ENCHANTING) && !bot->HasItemCount(16207, 1, true))
StoreItem(16207, 1); // Runed Arcanite Rod
}
if (bot->HasSkill(SKILL_SKINNING))
{
if (bot->HasSkill(SKILL_SKINNING) && !bot->HasItemCount(7005, 1, true))
StoreItem(7005, 1); // Skinning Knife
}
}
Item* PlayerbotFactory::StoreItem(uint32 itemId, uint32 count)
{

View File

@ -6,6 +6,9 @@
#ifndef _PLAYERBOT_PLAYERBOTFACTORY_H
#define _PLAYERBOT_PLAYERBOTFACTORY_H
#include <string>
#include <utility>
#include "InventoryAction.h"
#include "Player.h"
#include "PlayerbotAI.h"
@ -87,12 +90,91 @@ public:
void InitAttunementQuests();
private:
enum class ProfessionSpecializationSpell : uint32
{
Weapon = 9787,
Armor = 9788,
Hammer = 17040,
Axe = 17041,
Sword = 17039,
LearnWeapon = 9789,
LearnArmor = 9790,
LearnHammer = 39099,
LearnAxe = 39098,
LearnSword = 39097,
Dragon = 10656,
Elemental = 10658,
Tribal = 10660,
LearnDragon = 10657,
LearnElemental = 10659,
LearnTribal = 10661,
Spellfire = 26797,
Mooncloth = 26798,
Shadoweave = 26801,
Goblin = 20222,
Gnomish = 20219,
LearnGoblin = 20221,
LearnGnomish = 20220,
LearnSpellfire = 26796,
LearnMooncloth = 26799,
LearnShadoweave = 26800,
Transmute = 28672,
Elixir = 28677,
Potion = 28675,
LearnTransmute = 28674,
LearnElixir = 28678,
LearnPotion = 28676
};
enum class ProfessionRollType : uint32
{
Random = 1,
Class = 2
};
struct WeightedProfessionPair
{
uint16 firstSkill;
uint16 secondSkill;
uint32 weight;
};
void Prepare();
// void InitSecondEquipmentSet();
// void InitEquipmentNew(bool incremental);
bool CanEquipItem(ItemTemplate const* proto);
bool CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item);
static bool IsPrimaryTradeSkill(uint16 skillId);
static bool IsGatheringTradeSkill(uint16 skillId);
static bool IsCraftingTradeSkill(uint16 skillId);
static uint32 GetProfessionStarterSpell(uint16 skillId);
static std::vector<WeightedProfessionPair> GetClassProfessionPairs(Player* bot);
static std::vector<WeightedProfessionPair> GetRandomProfessionPairs();
static std::pair<uint16, uint16> ChooseProfessionPair(std::vector<WeightedProfessionPair> const& professionPairs);
static bool HasProfessionPair(std::vector<WeightedProfessionPair> const& professionPairs,
uint16 firstSkill, uint16 secondSkill);
static uint16 ChooseSingleProfession(std::vector<WeightedProfessionPair> const& professionPairs);
static uint32 GetStoredOrRandomValue(Player* bot, std::string const& key, uint32 minValue, uint32 maxValue);
static bool HasAnySpell(Player* bot, std::vector<uint32> const& spells);
static bool LearnProfessionSpecialization(Player* bot,
ProfessionSpecializationSpell knownSpell,
ProfessionSpecializationSpell learnSpell);
void InitTradeSkills();
void InitTradeSpecializations();
bool InitAlchemySpecialization();
bool InitEngineeringSpecialization();
bool InitLeatherworkingSpecialization();
bool InitTailoringSpecialization();
bool InitBlacksmithingSpecialization();
void UpdateTradeSkills();
void SetRandomSkill(uint16 id);
void ClearSpells();

View File

@ -236,6 +236,8 @@ bool PlayerbotAIConfig::Initialize()
EnableICCBuffs = sConfigMgr->GetOption<bool>("AiPlayerbot.EnableICCBuffs", true);
//////////////////////////// Professions
classMatchingProfessionChance =
std::min<uint32>(100, sConfigMgr->GetOption<uint32>("AiPlayerbot.ClassMatchingProfessionChance", 30));
fishingDistanceFromMaster = sConfigMgr->GetOption<float>("AiPlayerbot.FishingDistanceFromMaster", 10.0f);
endFishingWithMaster = sConfigMgr->GetOption<float>("AiPlayerbot.EndFishingWithMaster", 30.0f);
fishingDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FishingDistance", 40.0f);

View File

@ -152,6 +152,7 @@ public:
// Professions
bool enableFishingWithMaster;
uint32 classMatchingProfessionChance;
float fishingDistanceFromMaster, fishingDistance, endFishingWithMaster;
// chat