Core/Professions: Export missing game symbols and wire crafting procs
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
|
||||
#include "Crafting.h"
|
||||
#include "Containers.h"
|
||||
#include "Log.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "DB2Structure.h"
|
||||
#include "Item.h"
|
||||
@@ -28,7 +29,10 @@
|
||||
#include "Spell.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "CraftingPacketsCommon.h"
|
||||
#include "Random.h"
|
||||
#include "SpellPackets.h"
|
||||
#include "UpdateFields.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
@@ -111,6 +115,47 @@ namespace
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
struct CraftingQualityMatch
|
||||
{
|
||||
int32 CraftingQualityID = 0;
|
||||
uint32 Tier = 0;
|
||||
float NextPercentage = 0.0f;
|
||||
};
|
||||
|
||||
CraftingQualityMatch MatchCraftingQuality(int32 craftingDifficultyID, float skillPercent)
|
||||
{
|
||||
CraftingQualityMatch match;
|
||||
float bestPercentage = -1.0f;
|
||||
bool foundNext = false;
|
||||
|
||||
for (CraftingDifficultyQualityEntry const* row : sCraftingDifficultyQualityStore)
|
||||
{
|
||||
if (row->CraftingDifficultyID != craftingDifficultyID)
|
||||
continue;
|
||||
|
||||
if (row->QualityPercentage <= skillPercent && row->QualityPercentage >= bestPercentage)
|
||||
{
|
||||
bestPercentage = row->QualityPercentage;
|
||||
match.CraftingQualityID = row->CraftingQualityID;
|
||||
if (CraftingQualityEntry const* quality = sCraftingQualityStore.LookupEntry(row->CraftingQualityID))
|
||||
match.Tier = uint32(std::max(0, quality->QualityTier));
|
||||
else
|
||||
match.Tier = 0;
|
||||
}
|
||||
|
||||
if (row->QualityPercentage > skillPercent && (!foundNext || row->QualityPercentage < match.NextPercentage))
|
||||
{
|
||||
match.NextPercentage = row->QualityPercentage;
|
||||
foundNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundNext)
|
||||
match.NextPercentage = 0.0f;
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
SpellCastResult Crafting::DoCraft(uint32 craftingDataId)
|
||||
@@ -241,22 +286,188 @@ SpellCastResult Crafting::DoCraft(uint32 craftingDataId)
|
||||
}
|
||||
|
||||
uint32 skillLevel = _player->GetSkillValue(GetSkillIdForSpell(_spell->GetSpellInfo()->Id));
|
||||
uint32 reagentSkill = 0;
|
||||
float baseDifficulty = float(_craftingData->CraftingDifficulty);
|
||||
if (CraftingDifficultyEntry const* craftingDifficultyEntry = sCraftingDifficultyStore.LookupEntry(_craftingData->CraftingDifficultyID))
|
||||
CraftingDifficultyEntry const* craftingDifficultyEntry = sCraftingDifficultyStore.LookupEntry(_craftingData->CraftingDifficultyID);
|
||||
if (craftingDifficultyEntry)
|
||||
{
|
||||
float bonusPercent = _recraft ? craftingDifficultyEntry->ReCraftSkillBonusPercent : craftingDifficultyEntry->CraftSkillBonusPercent;
|
||||
if (totalWeight > 0.0f && bonusPercent > 0.0f)
|
||||
{
|
||||
float maximumReagentBonus = std::round(baseDifficulty * bonusPercent / 100.0f);
|
||||
skillLevel += uint32(std::max(0L, std::lround(maximumReagentBonus * weightedEffect / totalWeight)));
|
||||
reagentSkill = uint32(std::max(0L, std::lround(maximumReagentBonus * weightedEffect / totalWeight)));
|
||||
skillLevel += reagentSkill;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 difficultyPercent = 100;
|
||||
if (difficulty > 0.0f)
|
||||
difficultyPercent = uint32(std::max(0L, std::lround(float(skillLevel) / difficulty * 100.0f)));
|
||||
SkillLineAbilityEntry const* skillAbility = nullptr;
|
||||
SkillLineAbilityMapBounds abilityBounds = sSpellMgr->GetSkillLineAbilityMapBounds(_spell->GetSpellInfo()->Id);
|
||||
for (SkillLineAbilityMap::const_iterator ability = abilityBounds.first; ability != abilityBounds.second; ++ability)
|
||||
{
|
||||
if (!skillAbility)
|
||||
skillAbility = ability->second;
|
||||
if (ability->second->SkillupSkillLineID)
|
||||
{
|
||||
skillAbility = ability->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 qualityTier = sDB2Manager.GetCraftingQualityTierByDifficultyPercent(_craftingData->CraftingDifficultyID, difficultyPercent);
|
||||
ProfessionEffectTotals traitEffects;
|
||||
auto visitSkillLineChain = [](uint32 skillLineId, auto&& visitor)
|
||||
{
|
||||
for (uint32 step = 0; skillLineId && step < 8; ++step)
|
||||
{
|
||||
if (visitor(skillLineId))
|
||||
return true;
|
||||
|
||||
SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(skillLineId);
|
||||
uint32 parent = skillLine ? skillLine->ParentSkillLineID : 0;
|
||||
if (!parent || parent == skillLineId)
|
||||
break;
|
||||
|
||||
skillLineId = parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (skillAbility)
|
||||
{
|
||||
std::unordered_set<uint32> skillLines;
|
||||
auto addChain = [&](uint32 skillLineId)
|
||||
{
|
||||
visitSkillLineChain(skillLineId, [&](uint32 line)
|
||||
{
|
||||
return !skillLines.insert(line).second;
|
||||
});
|
||||
};
|
||||
addChain(skillAbility->SkillLine);
|
||||
addChain(skillAbility->SkillupSkillLineID);
|
||||
|
||||
auto applyTraitEntry = [&](UF::TraitEntry const& entry)
|
||||
{
|
||||
int32 rank = entry.Rank + entry.GrantedRanks + entry.BonusRanks;
|
||||
if (rank <= 0 || entry.TraitNodeEntryID <= 0)
|
||||
return;
|
||||
|
||||
TraitNodeEntryEntry const* nodeEntry = sTraitNodeEntryStore.LookupEntry(entry.TraitNodeEntryID);
|
||||
if (!nodeEntry || nodeEntry->TraitDefinitionID <= 0)
|
||||
return;
|
||||
|
||||
sDB2Manager.AccumulateProfessionTraitEffects(nodeEntry->TraitDefinitionID, rank, traitEffects);
|
||||
};
|
||||
|
||||
for (auto const& [_, traitConfig] : _player->m_activePlayerData->TraitConfigs)
|
||||
{
|
||||
if (TraitConfigType(*traitConfig.value.Type) != TraitConfigType::Profession)
|
||||
continue;
|
||||
|
||||
int32 configSkillLine = traitConfig.value.SkillLineID;
|
||||
if (configSkillLine <= 0 || !skillLines.contains(uint32(configSkillLine)))
|
||||
continue;
|
||||
|
||||
for (UF::TraitEntry const& entry : traitConfig.value.Entries)
|
||||
applyTraitEntry(entry);
|
||||
|
||||
for (UF::TraitSubTreeCache const& subTree : traitConfig.value.SubTrees)
|
||||
for (UF::TraitEntry const& entry : subTree.Entries)
|
||||
applyTraitEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (traitEffects.Skill > 0.0f)
|
||||
skillLevel += uint32(std::lround(traitEffects.Skill));
|
||||
|
||||
difficulty += traitEffects.DifficultyDelta;
|
||||
if (difficulty < 1.0f)
|
||||
difficulty = 1.0f;
|
||||
|
||||
// Client profession UI scale shared by Dragonflight, The War Within and Midnight: 1100 rating is 100%.
|
||||
// This is not a spell id.
|
||||
constexpr float ProfessionRatingScale = 1100.0f;
|
||||
bool inspired = false;
|
||||
float critBonusSkill = 0.0f;
|
||||
if (traitEffects.InspirationRating > 0.0f && roll_chance(traitEffects.InspirationRating / ProfessionRatingScale * 100.0f))
|
||||
{
|
||||
inspired = true;
|
||||
float bonusPercent = _craftingData->InspirationSkillBonusPercent;
|
||||
if (bonusPercent <= 0.0f && craftingDifficultyEntry)
|
||||
bonusPercent = craftingDifficultyEntry->InspirationSkillBonusPercent;
|
||||
|
||||
critBonusSkill = difficulty * bonusPercent / 100.0f + traitEffects.InspirationBonusSkill;
|
||||
if (critBonusSkill > 0.0f)
|
||||
skillLevel += uint32(std::lround(critBonusSkill));
|
||||
}
|
||||
|
||||
auto skillPercent = [&]()
|
||||
{
|
||||
return float(skillLevel) / difficulty * 100.0f;
|
||||
};
|
||||
|
||||
uint32 concentrationCurrency = 0;
|
||||
if (skillAbility)
|
||||
{
|
||||
auto findCurrency = [&](uint32 skillLineId)
|
||||
{
|
||||
return visitSkillLineChain(skillLineId, [&](uint32 line)
|
||||
{
|
||||
concentrationCurrency = sDB2Manager.GetConcentrationCurrencyForSkillLine(line);
|
||||
return concentrationCurrency != 0;
|
||||
});
|
||||
};
|
||||
|
||||
if (!findCurrency(skillAbility->SkillLine))
|
||||
findCurrency(skillAbility->SkillupSkillLineID);
|
||||
}
|
||||
|
||||
int32 concentrationSpent = 0;
|
||||
bool concentrationApplied = false;
|
||||
CraftingQualityMatch quality = MatchCraftingQuality(_craftingData->CraftingDifficultyID, skillPercent());
|
||||
if (_spell->m_targets.ApplyConcentration && quality.NextPercentage > skillPercent())
|
||||
{
|
||||
bool haveCurves = craftingDifficultyEntry
|
||||
&& craftingDifficultyEntry->ConcentrationSkillCurveID > 0
|
||||
&& craftingDifficultyEntry->ConcentrationDifficultyCurveID > 0;
|
||||
float skillFactor = 0.0f;
|
||||
float difficultyFactor = 0.0f;
|
||||
if (haveCurves)
|
||||
{
|
||||
skillFactor = sDB2Manager.GetCurveValueAt(uint32(craftingDifficultyEntry->ConcentrationSkillCurveID), skillPercent());
|
||||
difficultyFactor = sDB2Manager.GetCurveValueAt(uint32(craftingDifficultyEntry->ConcentrationDifficultyCurveID), difficulty);
|
||||
}
|
||||
|
||||
float reduction = traitEffects.ConcentrationCostReductionPercent;
|
||||
if (reduction < 0.0f)
|
||||
reduction = 0.0f;
|
||||
else if (reduction > 100.0f)
|
||||
reduction = 100.0f;
|
||||
|
||||
int32 cost = 0;
|
||||
if (skillFactor > 0.0f && difficultyFactor > 0.0f)
|
||||
cost = int32(std::lround(skillFactor * difficultyFactor * (1.0f - reduction / 100.0f)));
|
||||
|
||||
if (!haveCurves || !concentrationCurrency || cost <= 0)
|
||||
{
|
||||
TC_LOG_DEBUG("spells", "Spell {} crafted without concentration because the profession currency or cost curve is missing.", _spell->GetSpellInfo()->Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32 reserved = 0;
|
||||
if (auto reservedCurrency = usedCurrencies.find(concentrationCurrency); reservedCurrency != usedCurrencies.end())
|
||||
reserved = reservedCurrency->second;
|
||||
|
||||
if (!_player->HasCurrency(concentrationCurrency, reserved + uint32(cost)))
|
||||
return SpellCastResult::SPELL_FAILED_NOT_ENOUGH_CURRENCY;
|
||||
|
||||
skillLevel = std::max(skillLevel, uint32(std::ceil(difficulty * quality.NextPercentage / 100.0f)));
|
||||
concentrationSpent = cost;
|
||||
concentrationApplied = true;
|
||||
quality = MatchCraftingQuality(_craftingData->CraftingDifficultyID, skillPercent());
|
||||
}
|
||||
}
|
||||
|
||||
uint32 qualityTier = quality.Tier;
|
||||
uint32 craftedItemId = GetCraftedItemIdForQuality(qualityTier);
|
||||
if (!craftedItemId)
|
||||
return SpellCastResult::SPELL_FAILED_ERROR;
|
||||
@@ -278,9 +489,46 @@ SpellCastResult Crafting::DoCraft(uint32 craftingDataId)
|
||||
statsChosenByReagent = true;
|
||||
}
|
||||
|
||||
uint32 craftCount = 1;
|
||||
uint32 multicraftExtra = 0;
|
||||
if (itemTemplate && itemTemplate->GetMaxStackSize() > 1 && traitEffects.MulticraftRating > 0.0f
|
||||
&& roll_chance(traitEffects.MulticraftRating / ProfessionRatingScale * 100.0f))
|
||||
{
|
||||
float minExtra = 0.0f;
|
||||
float maxExtra = 0.0f;
|
||||
bool haveRange = false;
|
||||
if (skillAbility)
|
||||
{
|
||||
auto findRange = [&](uint32 skillLineId)
|
||||
{
|
||||
return visitSkillLineChain(skillLineId, [&](uint32 line)
|
||||
{
|
||||
haveRange = sDB2Manager.GetMulticraftExtraQuantityRange(line, minExtra, maxExtra);
|
||||
return haveRange;
|
||||
});
|
||||
};
|
||||
|
||||
if (!findRange(skillAbility->SkillLine))
|
||||
findRange(skillAbility->SkillupSkillLineID);
|
||||
}
|
||||
|
||||
if (haveRange)
|
||||
{
|
||||
maxExtra += traitEffects.MulticraftExtraPercent;
|
||||
if (maxExtra < minExtra)
|
||||
maxExtra = minExtra;
|
||||
|
||||
float rolled = frand(minExtra, maxExtra);
|
||||
multicraftExtra = std::max<uint32>(1, uint32(std::lround(float(craftCount) * rolled / 100.0f)));
|
||||
}
|
||||
else
|
||||
multicraftExtra = 1;
|
||||
}
|
||||
|
||||
uint32 totalCount = craftCount + multicraftExtra;
|
||||
ItemPosCountVec dest;
|
||||
uint32 noSpace = 0;
|
||||
InventoryResult msg = _player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, craftedItemId, 1, &noSpace);
|
||||
InventoryResult msg = _player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, craftedItemId, totalCount, &noSpace);
|
||||
if (msg != EQUIP_ERR_OK)
|
||||
{
|
||||
_player->SendEquipError(msg, nullptr, nullptr, craftedItemId);
|
||||
@@ -296,13 +544,40 @@ SpellCastResult Crafting::DoCraft(uint32 craftingDataId)
|
||||
if (createdItem->GetTemplate()->HasSignature())
|
||||
createdItem->SetCreator(_player->GetGUID());
|
||||
|
||||
if (concentrationApplied)
|
||||
usedCurrencies[concentrationCurrency] += uint32(concentrationSpent);
|
||||
|
||||
for (auto const& [itemId, itemCount] : usedItems)
|
||||
_player->DestroyItemCount(itemId, itemCount, true);
|
||||
|
||||
for (auto const& [currencyId, currencyCount] : usedCurrencies)
|
||||
_player->ModifyCurrency(currencyId, -int32(currencyCount), CurrencyGainSource::Spell, CurrencyDestroyReason::Spell);
|
||||
|
||||
_player->SendNewItem(createdItem, 1, true, true);
|
||||
float progress = float(skillLevel) / difficulty;
|
||||
if (progress < 0.0f)
|
||||
progress = 0.0f;
|
||||
else if (progress > 1.0f)
|
||||
progress = 1.0f;
|
||||
|
||||
WorldPackets::Crafting::CraftingData craftingData;
|
||||
craftingData.CraftingQualityID = quality.CraftingQualityID;
|
||||
craftingData.QualityProgress = progress;
|
||||
craftingData.SkillLineAbilityID = skillAbility ? int32(skillAbility->ID) : 0;
|
||||
craftingData.CraftingDataID = int32(_craftingData->ID);
|
||||
craftingData.Multicraft = int32(multicraftExtra);
|
||||
craftingData.SkillFromReagents = int32(reagentSkill);
|
||||
craftingData.Skill = int32(skillLevel);
|
||||
craftingData.CritBonusSkill = inspired ? int32(std::lround(critBonusSkill)) : 0;
|
||||
craftingData.IsCrit = inspired;
|
||||
craftingData.IsRecraft = _recraft;
|
||||
craftingData.ItemGUID = createdItem->GetGUID();
|
||||
craftingData.Quantity = int32(totalCount);
|
||||
craftingData.NewItem.Initialize(createdItem);
|
||||
craftingData.ConcentrationCurrencyID = concentrationApplied ? int32(concentrationCurrency) : 0;
|
||||
craftingData.ConcentrationSpent = concentrationSpent;
|
||||
craftingData.ApplyConcentration = concentrationApplied;
|
||||
|
||||
_player->SendNewItem(createdItem, totalCount, true, true, false, 0, &craftingData);
|
||||
_player->UpdateCraftSkill(_spell->GetSpellInfo());
|
||||
return SpellCastResult::SPELL_CAST_OK;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
//WowCommunity
|
||||
#include "DB2FileSystemSource.h"
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
//WowCommunity
|
||||
|
||||
DB2Storage<AchievementEntry> sAchievementStore("Achievement.db2", &AchievementLoadInfo::Instance);
|
||||
DB2Storage<Achievement_CategoryEntry> sAchievementCategoryStore("Achievement_Category.db2", &AchievementCategoryLoadInfo::Instance);
|
||||
@@ -727,6 +732,322 @@ namespace
|
||||
std::unordered_map<uint32, std::set<ItemSparseEntry const*>> _ItemSparseByMCRReagentItem;
|
||||
std::unordered_map<uint32, std::vector<uint32>> _CraftingDataItemIDByCraftingData;
|
||||
std::unordered_map<uint32, CraftingReagentQualityEntry const*> _CraftingReagentQualityByItem;
|
||||
|
||||
//WowCommunity
|
||||
struct ProfessionEffectValue
|
||||
{
|
||||
int32 Type = 0;
|
||||
float Amount = 0.0f;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32, std::vector<ProfessionEffectValue>> _professionEffectsByTraitDefinition;
|
||||
std::unordered_map<uint32, uint32> _concentrationCurrencyBySkillLine;
|
||||
std::unordered_map<uint32, std::pair<float, float>> _multicraftExtraPercentBySkillLine;
|
||||
|
||||
// Client files only. These tables have no hotfix statement, so a missing or
|
||||
// mismatched db2 must not be added to the fatal load error list.
|
||||
void LoadProfessionProcTables(std::string const& path)
|
||||
{
|
||||
#pragma pack(push, 1)
|
||||
struct ProfessionRecord
|
||||
{
|
||||
uint32 ID;
|
||||
int32 SkillLineID;
|
||||
int32 ProfessionEnumValue;
|
||||
float ResourcefulEffectLow;
|
||||
float ResourcefulEffectHigh;
|
||||
float MinCraftExtraQtyPct;
|
||||
float MaxCraftExtraQtyPct;
|
||||
float GatherTimeIncreasePct;
|
||||
int32 ActionTypeEnumValue;
|
||||
float RecraftModSkillGainPct;
|
||||
int32 PublicOrderCapacityCurrencyID;
|
||||
};
|
||||
|
||||
struct ProfessionEffectRecord
|
||||
{
|
||||
uint32 ID;
|
||||
int32 ProfessionEffectTypeEnumID;
|
||||
float Amount;
|
||||
int32 ModifiedCraftingReagentSlotID;
|
||||
};
|
||||
|
||||
struct ProfessionTraitRecord
|
||||
{
|
||||
uint32 ID;
|
||||
int32 TraitDefinitionID;
|
||||
};
|
||||
|
||||
struct ProfessionTraitXEffectRecord
|
||||
{
|
||||
uint32 ID;
|
||||
int32 ProfessionTraitID;
|
||||
int32 ProfessionEffectID;
|
||||
int32 Unused;
|
||||
};
|
||||
|
||||
struct ProfessionExpansionRecord
|
||||
{
|
||||
uint32 ID;
|
||||
int32 SkillLineID;
|
||||
int32 RecraftSpellID;
|
||||
int32 ConcentrationCurrencyID;
|
||||
int32 ExpansionID;
|
||||
int32 SpecResetSpellID;
|
||||
int32 SpecResetCurrencyID;
|
||||
int32 ProfessionID;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(ProfessionRecord) == 44);
|
||||
static_assert(sizeof(ProfessionEffectRecord) == 16);
|
||||
static_assert(sizeof(ProfessionTraitRecord) == 8);
|
||||
static_assert(sizeof(ProfessionTraitXEffectRecord) == 16);
|
||||
static_assert(sizeof(ProfessionExpansionRecord) == 32);
|
||||
|
||||
// Layout hashes are the 12.1.0.69875 WoWDBDefs LAYOUT ids.
|
||||
static const DB2MetaField ProfessionFields[11] =
|
||||
{
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
};
|
||||
static const DB2Meta ProfessionMeta =
|
||||
{
|
||||
.FileDataId = 4508544,
|
||||
.IndexField = 0,
|
||||
.ParentIndexField = -1,
|
||||
.FieldCount = 11,
|
||||
.FileFieldCount = 11,
|
||||
.LayoutHash = 0x86FE926D,
|
||||
.Fields = ProfessionFields
|
||||
};
|
||||
static const DB2FieldMeta ProfessionLoadFields[11] =
|
||||
{
|
||||
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "SkillLineID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ProfessionEnumValue" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "ResourcefulEffectLow" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "ResourcefulEffectHigh" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "MinCraftExtraQtyPct" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "MaxCraftExtraQtyPct" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "GatherTimeIncreasePct" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ActionTypeEnumValue" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "RecraftModSkillGainPct" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "PublicOrderCapacityCurrencyID" },
|
||||
};
|
||||
static const DB2FileLoadInfo ProfessionLoadInfo{ ProfessionLoadFields, 11, &ProfessionMeta };
|
||||
|
||||
static const DB2MetaField ProfessionEffectFields[3] =
|
||||
{
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_FLOAT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
};
|
||||
static const DB2Meta ProfessionEffectMeta =
|
||||
{
|
||||
.FileDataId = 4505297,
|
||||
.IndexField = -1,
|
||||
.ParentIndexField = -1,
|
||||
.FieldCount = 3,
|
||||
.FileFieldCount = 3,
|
||||
.LayoutHash = 0x04C16FA9,
|
||||
.Fields = ProfessionEffectFields
|
||||
};
|
||||
static const DB2FieldMeta ProfessionEffectLoadFields[4] =
|
||||
{
|
||||
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ProfessionEffectTypeEnumID" },
|
||||
{.IsSigned = false, .Type = FT_FLOAT, .Name = "Amount" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ModifiedCraftingReagentSlotID" },
|
||||
};
|
||||
static const DB2FileLoadInfo ProfessionEffectLoadInfo{ ProfessionEffectLoadFields, 4, &ProfessionEffectMeta };
|
||||
|
||||
static const DB2MetaField ProfessionTraitFields[2] =
|
||||
{
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
};
|
||||
static const DB2Meta ProfessionTraitMeta =
|
||||
{
|
||||
.FileDataId = 4505298,
|
||||
.IndexField = 0,
|
||||
.ParentIndexField = -1,
|
||||
.FieldCount = 2,
|
||||
.FileFieldCount = 2,
|
||||
.LayoutHash = 0x306455FE,
|
||||
.Fields = ProfessionTraitFields
|
||||
};
|
||||
static const DB2FieldMeta ProfessionTraitLoadFields[2] =
|
||||
{
|
||||
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "TraitDefinitionID" },
|
||||
};
|
||||
static const DB2FileLoadInfo ProfessionTraitLoadInfo{ ProfessionTraitLoadFields, 2, &ProfessionTraitMeta };
|
||||
|
||||
static const DB2MetaField ProfessionTraitXEffectFields[4] =
|
||||
{
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
};
|
||||
static const DB2Meta ProfessionTraitXEffectMeta =
|
||||
{
|
||||
.FileDataId = 4505494,
|
||||
.IndexField = 0,
|
||||
.ParentIndexField = 1,
|
||||
.FieldCount = 4,
|
||||
.FileFieldCount = 4,
|
||||
.LayoutHash = 0x97A0A286,
|
||||
.Fields = ProfessionTraitXEffectFields
|
||||
};
|
||||
static const DB2FieldMeta ProfessionTraitXEffectLoadFields[4] =
|
||||
{
|
||||
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ProfessionTraitID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ProfessionEffectID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "Field_10_0_0_44649_003" },
|
||||
};
|
||||
static const DB2FileLoadInfo ProfessionTraitXEffectLoadInfo{ ProfessionTraitXEffectLoadFields, 4, &ProfessionTraitXEffectMeta };
|
||||
|
||||
static const DB2MetaField ProfessionExpansionFields[8] =
|
||||
{
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = false },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
{.Type = FT_INT, .ArraySize = 1, .IsSigned = true },
|
||||
};
|
||||
static const DB2Meta ProfessionExpansionMeta =
|
||||
{
|
||||
.FileDataId = 5662322,
|
||||
.IndexField = 0,
|
||||
.ParentIndexField = 7,
|
||||
.FieldCount = 8,
|
||||
.FileFieldCount = 7,
|
||||
.LayoutHash = 0x6DABCB98,
|
||||
.Fields = ProfessionExpansionFields
|
||||
};
|
||||
static const DB2FieldMeta ProfessionExpansionLoadFields[8] =
|
||||
{
|
||||
{.IsSigned = false, .Type = FT_INT, .Name = "ID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "SkillLineID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "RecraftSpellID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ConcentrationCurrencyID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ExpansionID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "SpecResetSpellID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "SpecResetCurrencyID" },
|
||||
{.IsSigned = true, .Type = FT_INT, .Name = "ProfessionID" },
|
||||
};
|
||||
static const DB2FileLoadInfo ProfessionExpansionLoadInfo{ ProfessionExpansionLoadFields, 8, &ProfessionExpansionMeta };
|
||||
|
||||
auto loadRecords = [&](char const* fileName, DB2FileLoadInfo const* loadInfo, auto& out)
|
||||
{
|
||||
using Record = typename std::decay_t<decltype(out)>::value_type;
|
||||
try
|
||||
{
|
||||
DB2FileLoader loader;
|
||||
DB2FileSystemSource source(path + fileName);
|
||||
loader.Load(&source, loadInfo);
|
||||
|
||||
uint32 indexSize = 0;
|
||||
char** index = nullptr;
|
||||
char* data = loader.AutoProduceData(indexSize, index);
|
||||
loader.AutoProduceRecordCopies(indexSize, index, data);
|
||||
|
||||
if (loadInfo->Meta->GetRecordSize() != sizeof(Record))
|
||||
{
|
||||
delete[] data;
|
||||
delete[] index;
|
||||
TC_LOG_ERROR("server.loading", "Profession table {} record size {} does not match C++ size {}", fileName, loadInfo->Meta->GetRecordSize(), sizeof(Record));
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32 i = 0; i < indexSize; ++i)
|
||||
{
|
||||
if (!index[i])
|
||||
continue;
|
||||
|
||||
Record record;
|
||||
std::memcpy(&record, index[i], sizeof(Record));
|
||||
out.push_back(record);
|
||||
}
|
||||
|
||||
delete[] data;
|
||||
delete[] index;
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} rows from {}", out.size(), fileName);
|
||||
}
|
||||
catch (std::exception const& error)
|
||||
{
|
||||
TC_LOG_ERROR("server.loading", "Profession table {} was not loaded ({}). Concentration, inspiration and multicraft stay off for data from this file.", fileName, error.what());
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<ProfessionRecord> professions;
|
||||
std::vector<ProfessionEffectRecord> effects;
|
||||
std::vector<ProfessionTraitRecord> traits;
|
||||
std::vector<ProfessionTraitXEffectRecord> links;
|
||||
std::vector<ProfessionExpansionRecord> expansions;
|
||||
loadRecords("Profession.db2", &ProfessionLoadInfo, professions);
|
||||
loadRecords("ProfessionEffect.db2", &ProfessionEffectLoadInfo, effects);
|
||||
loadRecords("ProfessionTrait.db2", &ProfessionTraitLoadInfo, traits);
|
||||
loadRecords("ProfessionTraitXEffect.db2", &ProfessionTraitXEffectLoadInfo, links);
|
||||
loadRecords("ProfessionExpansion.db2", &ProfessionExpansionLoadInfo, expansions);
|
||||
|
||||
if (professions.empty() || effects.empty() || traits.empty() || links.empty() || expansions.empty())
|
||||
{
|
||||
TC_LOG_ERROR("server.loading", "Profession proc tables are incomplete. Concentration, inspiration and multicraft stay disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32, ProfessionEffectValue> effectsById;
|
||||
for (ProfessionEffectRecord const& effect : effects)
|
||||
effectsById[effect.ID] = { effect.ProfessionEffectTypeEnumID, effect.Amount };
|
||||
|
||||
std::unordered_map<uint32, uint32> traitDefinitionById;
|
||||
for (ProfessionTraitRecord const& trait : traits)
|
||||
if (trait.TraitDefinitionID > 0)
|
||||
traitDefinitionById[trait.ID] = uint32(trait.TraitDefinitionID);
|
||||
|
||||
for (ProfessionTraitXEffectRecord const& link : links)
|
||||
{
|
||||
if (link.ProfessionTraitID <= 0 || link.ProfessionEffectID <= 0)
|
||||
continue;
|
||||
|
||||
auto trait = traitDefinitionById.find(uint32(link.ProfessionTraitID));
|
||||
auto effect = effectsById.find(uint32(link.ProfessionEffectID));
|
||||
if (trait == traitDefinitionById.end() || effect == effectsById.end())
|
||||
continue;
|
||||
|
||||
_professionEffectsByTraitDefinition[trait->second].push_back(effect->second);
|
||||
}
|
||||
|
||||
for (ProfessionRecord const& profession : professions)
|
||||
{
|
||||
if (profession.SkillLineID <= 0)
|
||||
continue;
|
||||
|
||||
if (profession.MinCraftExtraQtyPct > 0.0f || profession.MaxCraftExtraQtyPct > 0.0f)
|
||||
_multicraftExtraPercentBySkillLine[uint32(profession.SkillLineID)] = { profession.MinCraftExtraQtyPct, profession.MaxCraftExtraQtyPct };
|
||||
}
|
||||
|
||||
for (ProfessionExpansionRecord const& expansion : expansions)
|
||||
if (expansion.SkillLineID > 0 && expansion.ConcentrationCurrencyID > 0)
|
||||
_concentrationCurrencyBySkillLine[uint32(expansion.SkillLineID)] = uint32(expansion.ConcentrationCurrencyID);
|
||||
}
|
||||
//WowCommunity
|
||||
}
|
||||
|
||||
static void LoadDB2(std::bitset<TOTAL_LOCALES>& availableDb2Locales, std::vector<std::string>& errlist, StorageMap& stores, DB2StorageBase* storage, std::string const& db2Path,
|
||||
@@ -1378,6 +1699,7 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
|
||||
LOAD_DB2(sItemLogicalCostStore);
|
||||
LOAD_DB2(sTrophyStore);
|
||||
LOAD_DB2(sManagedWorldStateBuffStore);
|
||||
LoadProfessionProcTables(db2Path + localeNames[defaultLocale] + '/');
|
||||
//WowCommunity
|
||||
// error checks
|
||||
|
||||
@@ -3971,6 +4293,69 @@ uint32 DB2Manager::GetCraftingQualityTierByDifficultyPercent(int32 craftingDiffi
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void DB2Manager::AccumulateProfessionTraitEffects(int32 traitDefinitionId, int32 rank, ProfessionEffectTotals& totals) const
|
||||
{
|
||||
if (traitDefinitionId <= 0 || rank <= 0)
|
||||
return;
|
||||
|
||||
auto itr = _professionEffectsByTraitDefinition.find(uint32(traitDefinitionId));
|
||||
if (itr == _professionEffectsByTraitDefinition.end())
|
||||
return;
|
||||
|
||||
// ProfessionEffectType.EnumID, build 12.1.0.69875. Amount is per rank.
|
||||
for (ProfessionEffectValue const& effect : itr->second)
|
||||
{
|
||||
float value = effect.Amount * float(rank);
|
||||
switch (effect.Type)
|
||||
{
|
||||
case 0: // Skill
|
||||
totals.Skill += value;
|
||||
break;
|
||||
case 1: // Inspiration rating
|
||||
totals.InspirationRating += value;
|
||||
break;
|
||||
case 7: // Multicraft rating
|
||||
totals.MulticraftRating += value;
|
||||
break;
|
||||
case 18: // additional items crafted with Multicraft, percent
|
||||
totals.MulticraftExtraPercent += value;
|
||||
break;
|
||||
case 20: // bonus Crafting Skill from Inspiration
|
||||
totals.InspirationBonusSkill += value;
|
||||
break;
|
||||
case 22: // Decrease Difficulty
|
||||
totals.DifficultyDelta -= value;
|
||||
break;
|
||||
case 23: // Increase Difficulty
|
||||
totals.DifficultyDelta += value;
|
||||
break;
|
||||
case 27: // reduce concentration cost, percent
|
||||
totals.ConcentrationCostReductionPercent += value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32 DB2Manager::GetConcentrationCurrencyForSkillLine(uint32 skillLineId) const
|
||||
{
|
||||
auto itr = _concentrationCurrencyBySkillLine.find(skillLineId);
|
||||
return itr != _concentrationCurrencyBySkillLine.end() ? itr->second : 0;
|
||||
}
|
||||
|
||||
bool DB2Manager::GetMulticraftExtraQuantityRange(uint32 skillLineId, float& minPercent, float& maxPercent) const
|
||||
{
|
||||
auto itr = _multicraftExtraPercentBySkillLine.find(skillLineId);
|
||||
if (itr == _multicraftExtraPercentBySkillLine.end())
|
||||
return false;
|
||||
|
||||
minPercent = itr->second.first;
|
||||
maxPercent = itr->second.second;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 DB2Manager::GetConduitForItem(uint32 itemId) const
|
||||
{
|
||||
auto itr = _conduitsByItem.find(itemId);
|
||||
|
||||
@@ -541,6 +541,20 @@ TC_GAME_API extern TaxiPathNodesByPath sTaxiPathNod
|
||||
static bool Compare(structure const* left, structure const* right); \
|
||||
};
|
||||
|
||||
|
||||
// Sum of ProfessionEffect rows reached through the player's profession trait config.
|
||||
// Amounts are already multiplied by rank. Effect types are ProfessionEffectType.EnumID.
|
||||
struct ProfessionEffectTotals
|
||||
{
|
||||
float Skill = 0.0f;
|
||||
float InspirationRating = 0.0f;
|
||||
float MulticraftRating = 0.0f;
|
||||
float MulticraftExtraPercent = 0.0f;
|
||||
float InspirationBonusSkill = 0.0f;
|
||||
float ConcentrationCostReductionPercent = 0.0f;
|
||||
float DifficultyDelta = 0.0f;
|
||||
};
|
||||
|
||||
class TC_GAME_API DB2Manager
|
||||
{
|
||||
public:
|
||||
@@ -737,6 +751,9 @@ public:
|
||||
std::vector<uint32> GetCraftingDataItemIDByCraftingData(uint32 craftingDataID);
|
||||
CraftingReagentQualityEntry const* GetCraftingReagentQualityByItem(uint32 itemID);
|
||||
uint32 GetCraftingQualityTierByDifficultyPercent(int32 craftingDifficultyID, uint32 difficultyPercent);
|
||||
void AccumulateProfessionTraitEffects(int32 traitDefinitionId, int32 rank, ProfessionEffectTotals& totals) const;
|
||||
uint32 GetConcentrationCurrencyForSkillLine(uint32 skillLineId) const;
|
||||
bool GetMulticraftExtraQuantityRange(uint32 skillLineId, float& minPercent, float& maxPercent) const;
|
||||
std::vector<RenownRewardsEntry const*> const* GetRenownRewards(int32 covenantId, int32 level) const;
|
||||
std::vector<BountyEntry const*> const* GetBountiesForBountySet(int32 bountySetId) const;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "ObjectGuid.h"
|
||||
#include <atomic>
|
||||
|
||||
class ObjectGuidGenerator
|
||||
class TC_GAME_API ObjectGuidGenerator
|
||||
{
|
||||
public:
|
||||
explicit ObjectGuidGenerator(HighGuid high, ObjectGuid::LowType start = UI64LIT(1)) : _nextGuid(start), _high(high) { }
|
||||
|
||||
@@ -14021,7 +14021,7 @@ void Player::SendItemPassives()
|
||||
SendDirectMessage(sendItemPassives.Write());
|
||||
}
|
||||
|
||||
void Player::SendNewItem(Item* item, uint32 quantity, bool pushed, bool created, bool broadcast /*= false*/, uint32 dungeonEncounterId /*= 0*/)
|
||||
void Player::SendNewItem(Item* item, uint32 quantity, bool pushed, bool created, bool broadcast /*= false*/, uint32 dungeonEncounterId /*= 0*/, WorldPackets::Crafting::CraftingData const* craftingData /*= nullptr*/)
|
||||
{
|
||||
if (!item) // prevent crash
|
||||
return;
|
||||
@@ -14047,6 +14047,8 @@ void Player::SendNewItem(Item* item, uint32 quantity, bool pushed, bool created,
|
||||
packet.BattlePetLevel = item->GetModifier(ITEM_MODIFIER_BATTLE_PET_LEVEL);
|
||||
|
||||
packet.ItemGUID = item->GetGUID();
|
||||
if (craftingData)
|
||||
packet.CraftingData = *craftingData;
|
||||
|
||||
packet.Pushed = pushed;
|
||||
packet.ChatNotifyType = WorldPackets::Item::ItemPushResult::DISPLAY_TYPE_NORMAL;
|
||||
|
||||
@@ -1724,7 +1724,7 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
|
||||
bool IsUseEquipedWeapon(bool mainhand) const;
|
||||
bool IsTwoHandUsed() const;
|
||||
bool IsUsingTwoHandedWeaponInOneHand() const;
|
||||
void SendNewItem(Item* item, uint32 quantity, bool pushed, bool created, bool broadcast = false, uint32 dungeonEncounterId = 0);
|
||||
void SendNewItem(Item* item, uint32 quantity, bool pushed, bool created, bool broadcast = false, uint32 dungeonEncounterId = 0, WorldPackets::Crafting::CraftingData const* craftingData = nullptr);
|
||||
bool BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uint32 item, uint32 count, uint8 bag, uint8 slot);
|
||||
Optional<SellResult> CanSellItemToVendor(Item const* item, uint32 amount) const;
|
||||
Optional<SellResult> SellItemToVendor(Item* item, uint32 amount);
|
||||
|
||||
@@ -153,6 +153,8 @@ SpellCastTargets::SpellCastTargets(Unit* caster, WorldPackets::Spells::SpellCast
|
||||
ModifiedCraftingReagents[uint32(reagent.Slot)].push_back(reagent);
|
||||
}
|
||||
|
||||
ApplyConcentration = (spellCastRequest.CraftingCastFlags & 1) != 0;
|
||||
|
||||
Update(caster);
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +452,7 @@ public:
|
||||
|
||||
//WowCommunity
|
||||
std::unordered_map<uint32 /*SpellSlot*/, std::vector<WorldPackets::Spells::SpellCraftingReagent>> ModifiedCraftingReagents;
|
||||
bool ApplyConcentration = false; // SpellCastRequest.CraftingCastFlags bit 0 (value 1)
|
||||
//WowCommunity
|
||||
|
||||
private:
|
||||
|
||||
@@ -91,7 +91,7 @@ struct boss_anubzekt : public BossAI
|
||||
me->GetRandomNearPosition(10.0f), TEMPSUMMON_CORPSE_TIMED_DESPAWN, 5s))
|
||||
{
|
||||
DoCastSelf(SPELL_SUMMON_WEBMAGE, true);
|
||||
webmage->SetInCombatWithZone();
|
||||
webmage->AI()->DoZoneInCombat();
|
||||
}
|
||||
events.Repeat(IsMythicPlus() ? 30s : 45s);
|
||||
break;
|
||||
|
||||
@@ -187,12 +187,12 @@ struct npc_starved_crawler : public ScriptedAI
|
||||
AttackStart(target);
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
void UpdateAI(uint32 /*diff*/) override
|
||||
{
|
||||
if (!UpdateVictim())
|
||||
return;
|
||||
|
||||
DoMeleeAttackIfReady();
|
||||
me->DoMeleeAttackIfReady();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ enum Misc
|
||||
SINGULARITY_ENERGY = 100
|
||||
};
|
||||
|
||||
enum BloodworkerEvents
|
||||
{
|
||||
EVENT_BLACK_BLOOD = 1
|
||||
};
|
||||
|
||||
struct boss_kikatal : public BossAI
|
||||
{
|
||||
boss_kikatal(Creature* creature) : BossAI(creature, DATA_KIKATAL_THE_HARVESTER) { }
|
||||
@@ -76,7 +81,7 @@ struct boss_kikatal : public BossAI
|
||||
if (Creature* bloodworker = me->SummonCreature(NPC_BLOODWORKER, pos, TEMPSUMMON_CORPSE_TIMED_DESPAWN, IsMythicPlus() ? 30s : 70s))
|
||||
{
|
||||
DoCastSelf(SPELL_CALL_DRONES, true);
|
||||
bloodworker->SetInCombatWithZone();
|
||||
bloodworker->AI()->DoZoneInCombat();
|
||||
}
|
||||
events.Repeat(IsMythicPlus() ? 20s : 35s);
|
||||
break;
|
||||
|
||||
@@ -14,7 +14,6 @@ static constexpr ObjectData creatureData[] =
|
||||
{ BOSS_AVANOXX, DATA_AVANOXX },
|
||||
{ BOSS_ANUBZEKT, DATA_ANUBZEKT },
|
||||
{ BOSS_KIKATAL_THE_HARVESTER, DATA_KIKATAL_THE_HARVESTER },
|
||||
{ 0, 0 } // END
|
||||
};
|
||||
|
||||
static constexpr DungeonEncounterData const encounters[] =
|
||||
@@ -35,7 +34,7 @@ public:
|
||||
{
|
||||
SetHeaders(DataHeader);
|
||||
SetBossNumber(EncounterCount);
|
||||
LoadObjectData(creatureData, nullptr);
|
||||
LoadObjectData(creatureData, {});
|
||||
LoadDungeonEncounterData(encounters);
|
||||
}
|
||||
|
||||
@@ -69,7 +68,7 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
bool faction_group = HORDE;
|
||||
Team faction_group = HORDE;
|
||||
};
|
||||
|
||||
InstanceScript* GetInstanceScript(InstanceMap* map) const override
|
||||
|
||||
Reference in New Issue
Block a user