3a890782b5
Port From (https://github.com/TrinityCore/TrinityCore/commit/53068a94e88991ae7196fb247df4b7ca2e47e554)
1046 lines
45 KiB
C#
1046 lines
45 KiB
C#
using Framework.Constants;
|
|
using Game.DataStorage;
|
|
using Game.Entities;
|
|
using Game.Networking.Packets;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Game
|
|
{
|
|
class TraitMgr
|
|
{
|
|
public static uint COMMIT_COMBAT_TRAIT_CONFIG_CHANGES_SPELL_ID = 384255u;
|
|
public static uint MAX_COMBAT_TRAIT_CONFIGS = 10u;
|
|
|
|
static Dictionary<uint, NodeGroup> _traitGroups = new();
|
|
static Dictionary<int, Node> _traitNodes = new();
|
|
static Dictionary<int, SubTree> _traitSubTrees = new();
|
|
static Dictionary<int, Tree> _traitTrees = new();
|
|
static uint[] _skillLinesByClass = new uint[(int)Class.Max];
|
|
static MultiMap<uint, Tree> _traitTreesBySkillLine = new();
|
|
static MultiMap<uint, Tree> _traitTreesByTraitSystem = new();
|
|
static int _configIdGenerator;
|
|
static MultiMap<uint, TraitCurrencySourceRecord> _traitCurrencySourcesByCurrency = new();
|
|
static MultiMap<uint, TraitDefinitionEffectPointsRecord> _traitDefinitionEffectPointModifiers = new();
|
|
static MultiMap<int, TraitTreeLoadoutEntryRecord> _traitTreeLoadoutsByChrSpecialization = new();
|
|
|
|
public static void Load()
|
|
{
|
|
_configIdGenerator = int.MaxValue;
|
|
|
|
MultiMap<uint, TraitCondRecord> nodeEntryConditions = new();
|
|
foreach (TraitNodeEntryXTraitCondRecord traitNodeEntryXTraitCondEntry in CliDB.TraitNodeEntryXTraitCondStorage.Values)
|
|
{
|
|
TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeEntryXTraitCondEntry.TraitCondID);
|
|
if (traitCondEntry != null)
|
|
nodeEntryConditions.Add(traitNodeEntryXTraitCondEntry.TraitNodeEntryID, traitCondEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitCostRecord> nodeEntryCosts = new();
|
|
foreach (TraitNodeEntryXTraitCostRecord traitNodeEntryXTraitCostEntry in CliDB.TraitNodeEntryXTraitCostStorage.Values)
|
|
{
|
|
TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeEntryXTraitCostEntry.TraitCostID);
|
|
if (traitCostEntry != null)
|
|
nodeEntryCosts.Add(traitNodeEntryXTraitCostEntry.TraitNodeEntryID, traitCostEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitCondRecord> nodeGroupConditions = new();
|
|
foreach (TraitNodeGroupXTraitCondRecord traitNodeGroupXTraitCondEntry in CliDB.TraitNodeGroupXTraitCondStorage.Values)
|
|
{
|
|
TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeGroupXTraitCondEntry.TraitCondID);
|
|
if (traitCondEntry != null)
|
|
nodeGroupConditions.Add(traitNodeGroupXTraitCondEntry.TraitNodeGroupID, traitCondEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitCostRecord> nodeGroupCosts = new();
|
|
foreach (TraitNodeGroupXTraitCostRecord traitNodeGroupXTraitCostEntry in CliDB.TraitNodeGroupXTraitCostStorage.Values)
|
|
{
|
|
TraitCostRecord traitCondEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeGroupXTraitCostEntry.TraitCostID);
|
|
if (traitCondEntry != null)
|
|
nodeGroupCosts.Add(traitNodeGroupXTraitCostEntry.TraitNodeGroupID, traitCondEntry);
|
|
}
|
|
|
|
MultiMap<int, uint> nodeGroups = new();
|
|
foreach (TraitNodeGroupXTraitNodeRecord traitNodeGroupXTraitNodeEntry in CliDB.TraitNodeGroupXTraitNodeStorage.Values)
|
|
nodeGroups.Add(traitNodeGroupXTraitNodeEntry.TraitNodeID, traitNodeGroupXTraitNodeEntry.TraitNodeGroupID);
|
|
|
|
MultiMap<uint, TraitCondRecord> nodeConditions = new();
|
|
foreach (TraitNodeXTraitCondRecord traitNodeXTraitCondEntry in CliDB.TraitNodeXTraitCondStorage.Values)
|
|
{
|
|
TraitCondRecord traitCondEntry = CliDB.TraitCondStorage.LookupByKey(traitNodeXTraitCondEntry.TraitCondID);
|
|
if (traitCondEntry != null)
|
|
nodeConditions.Add(traitNodeXTraitCondEntry.TraitNodeID, traitCondEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitCostRecord> nodeCosts = new();
|
|
foreach (TraitNodeXTraitCostRecord traitNodeXTraitCostEntry in CliDB.TraitNodeXTraitCostStorage.Values)
|
|
{
|
|
TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitNodeXTraitCostEntry.TraitCostID);
|
|
if (traitCostEntry != null)
|
|
nodeCosts.Add(traitNodeXTraitCostEntry.TraitNodeID, traitCostEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitNodeEntryRecord> nodeEntries = new();
|
|
foreach (TraitNodeXTraitNodeEntryRecord traitNodeXTraitNodeEntryEntry in CliDB.TraitNodeXTraitNodeEntryStorage.Values)
|
|
{
|
|
TraitNodeEntryRecord traitNodeEntryEntry = CliDB.TraitNodeEntryStorage.LookupByKey(traitNodeXTraitNodeEntryEntry.TraitNodeEntryID);
|
|
if (traitNodeEntryEntry != null)
|
|
nodeEntries.Add(traitNodeXTraitNodeEntryEntry.TraitNodeID, traitNodeEntryEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitCostRecord> treeCosts = new();
|
|
foreach (TraitTreeXTraitCostRecord traitTreeXTraitCostEntry in CliDB.TraitTreeXTraitCostStorage.Values)
|
|
{
|
|
TraitCostRecord traitCostEntry = CliDB.TraitCostStorage.LookupByKey(traitTreeXTraitCostEntry.TraitCostID);
|
|
if (traitCostEntry != null)
|
|
treeCosts.Add(traitTreeXTraitCostEntry.TraitTreeID, traitCostEntry);
|
|
}
|
|
|
|
MultiMap<uint, TraitTreeXTraitCurrencyRecord> treeCurrencies = new();
|
|
foreach (TraitTreeXTraitCurrencyRecord traitTreeXTraitCurrencyEntry in CliDB.TraitTreeXTraitCurrencyStorage.Values)
|
|
{
|
|
if (CliDB.TraitCurrencyStorage.HasRecord((uint)traitTreeXTraitCurrencyEntry.TraitCurrencyID))
|
|
treeCurrencies.Add(traitTreeXTraitCurrencyEntry.TraitTreeID, traitTreeXTraitCurrencyEntry);
|
|
}
|
|
|
|
MultiMap<uint, int> traitTreesIdsByTraitSystem = new();
|
|
foreach (TraitTreeRecord traitTree in CliDB.TraitTreeStorage.Values)
|
|
{
|
|
Tree tree = new();
|
|
tree.Data = traitTree;
|
|
|
|
var costs = treeCosts.LookupByKey(traitTree.Id);
|
|
if (costs != null)
|
|
tree.Costs = costs;
|
|
|
|
var currencies = treeCurrencies.LookupByKey(traitTree.Id);
|
|
if (currencies != null)
|
|
{
|
|
currencies.OrderBy(p => p.Index);
|
|
tree.Currencies.AddRange(currencies.Select(p => CliDB.TraitCurrencyStorage.LookupByKey(p.TraitCurrencyID)));
|
|
}
|
|
|
|
if (traitTree.TraitSystemID != 0)
|
|
{
|
|
traitTreesIdsByTraitSystem.Add(traitTree.TraitSystemID, (int)traitTree.Id);
|
|
tree.ConfigType = TraitConfigType.Generic;
|
|
}
|
|
|
|
_traitTrees[(int)traitTree.Id] = tree;
|
|
}
|
|
|
|
foreach (var (_, traitSubTree) in CliDB.TraitSubTreeStorage)
|
|
{
|
|
SubTree subTree = new();
|
|
subTree.Data = traitSubTree;
|
|
|
|
Tree tree = _traitTrees.LookupByKey(traitSubTree.TraitTreeID);
|
|
if (tree != null)
|
|
tree.SubTrees.Add(subTree);
|
|
|
|
_traitSubTrees[(int)traitSubTree.Id] = subTree;
|
|
}
|
|
|
|
foreach (var (_, traitNodeGroup) in CliDB.TraitNodeGroupStorage)
|
|
{
|
|
NodeGroup nodeGroup = new();
|
|
nodeGroup.Data = traitNodeGroup;
|
|
|
|
var conditions = nodeGroupConditions.LookupByKey(traitNodeGroup.Id);
|
|
if (conditions != null)
|
|
nodeGroup.Conditions = conditions;
|
|
|
|
var costs = nodeGroupCosts.LookupByKey(traitNodeGroup.Id);
|
|
if (costs != null)
|
|
nodeGroup.Costs = costs;
|
|
|
|
_traitGroups[traitNodeGroup.Id] = nodeGroup;
|
|
}
|
|
|
|
foreach (TraitNodeRecord traitNode in CliDB.TraitNodeStorage.Values)
|
|
{
|
|
Node node = new();
|
|
node.Data = traitNode;
|
|
|
|
Tree tree = _traitTrees.LookupByKey(traitNode.TraitTreeID);
|
|
if (tree != null)
|
|
tree.Nodes.Add(node);
|
|
|
|
foreach (var traitNodeEntry in nodeEntries.LookupByKey(traitNode.Id))
|
|
{
|
|
NodeEntry entry = new();
|
|
entry.Data = traitNodeEntry;
|
|
|
|
var conditions = nodeEntryConditions.LookupByKey(traitNodeEntry.Id);
|
|
if (conditions != null)
|
|
entry.Conditions = conditions;
|
|
|
|
var costs = nodeEntryCosts.LookupByKey(traitNodeEntry.Id);
|
|
if (costs != null)
|
|
entry.Costs = costs;
|
|
|
|
node.Entries.Add(entry);
|
|
}
|
|
|
|
foreach (var nodeGroupId in nodeGroups.LookupByKey(traitNode.Id))
|
|
{
|
|
NodeGroup nodeGroup = _traitGroups.LookupByKey(nodeGroupId);
|
|
if (nodeGroup == null)
|
|
continue;
|
|
|
|
nodeGroup.Nodes.Add(node);
|
|
node.Groups.Add(nodeGroup);
|
|
}
|
|
|
|
var conditions1 = nodeConditions.LookupByKey(traitNode.Id);
|
|
if (conditions1 != null)
|
|
node.Conditions = conditions1;
|
|
|
|
var costs1 = nodeCosts.LookupByKey(traitNode.Id);
|
|
if (costs1 != null)
|
|
node.Costs = costs1;
|
|
|
|
SubTree subTree = _traitSubTrees.LookupByKey(traitNode.TraitSubTreeID);
|
|
if (subTree != null)
|
|
{
|
|
subTree.Nodes.Add(node);
|
|
|
|
foreach (NodeEntry nodeEntry in node.Entries)
|
|
{
|
|
foreach (TraitCostRecord cost in nodeEntry.Costs)
|
|
{
|
|
TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID);
|
|
if (traitCurrency != null)
|
|
subTree.Currencies.Add(traitCurrency);
|
|
}
|
|
}
|
|
|
|
foreach (NodeGroup nodeGroup in node.Groups)
|
|
{
|
|
foreach (TraitCostRecord cost in nodeGroup.Costs)
|
|
{
|
|
TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID);
|
|
if (traitCurrency != null)
|
|
subTree.Currencies.Add(traitCurrency);
|
|
}
|
|
}
|
|
|
|
foreach (TraitCostRecord cost in node.Costs)
|
|
{
|
|
TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID);
|
|
if (traitCurrency != null)
|
|
subTree.Currencies.Add(traitCurrency);
|
|
}
|
|
|
|
Tree tree1 = _traitTrees.LookupByKey(traitNode.TraitTreeID);
|
|
if (tree1 != null)
|
|
{
|
|
foreach (TraitCostRecord cost in tree1.Costs)
|
|
{
|
|
TraitCurrencyRecord traitCurrency = CliDB.TraitCurrencyStorage.LookupByKey(cost.TraitCurrencyID);
|
|
if (traitCurrency != null)
|
|
subTree.Currencies.Add(traitCurrency);
|
|
}
|
|
}
|
|
}
|
|
|
|
_traitNodes[(int)traitNode.Id] = node;
|
|
}
|
|
|
|
foreach (TraitEdgeRecord traitEdgeEntry in CliDB.TraitEdgeStorage.Values)
|
|
{
|
|
Node left = _traitNodes.LookupByKey(traitEdgeEntry.LeftTraitNodeID);
|
|
Node right = _traitNodes.LookupByKey(traitEdgeEntry.RightTraitNodeID);
|
|
if (left == null || right == null)
|
|
continue;
|
|
|
|
right.ParentNodes.Add(Tuple.Create(left, (TraitEdgeType)traitEdgeEntry.Type));
|
|
}
|
|
|
|
foreach (SkillLineXTraitTreeRecord skillLineXTraitTreeEntry in CliDB.SkillLineXTraitTreeStorage.Values)
|
|
{
|
|
Tree tree = _traitTrees.LookupByKey(skillLineXTraitTreeEntry.TraitTreeID);
|
|
if (tree == null)
|
|
continue;
|
|
|
|
SkillLineRecord skillLineEntry = CliDB.SkillLineStorage.LookupByKey(skillLineXTraitTreeEntry.SkillLineID);
|
|
if (skillLineEntry == null)
|
|
continue;
|
|
|
|
_traitTreesBySkillLine.Add(skillLineXTraitTreeEntry.SkillLineID, tree);
|
|
if (skillLineEntry.CategoryID == SkillCategory.Class)
|
|
{
|
|
foreach (SkillRaceClassInfoRecord skillRaceClassInfo in Global.DB2Mgr.GetSkillRaceClassInfo(skillLineEntry.Id))
|
|
for (int i = 1; i < (int)Class.Max; ++i)
|
|
if ((skillRaceClassInfo.ClassMask & (1 << (i - 1))) != 0)
|
|
_skillLinesByClass[i] = skillLineXTraitTreeEntry.SkillLineID;
|
|
|
|
tree.ConfigType = TraitConfigType.Combat;
|
|
}
|
|
else
|
|
tree.ConfigType = TraitConfigType.Profession;
|
|
}
|
|
|
|
foreach (var (traitSystemId, traitTreeId) in traitTreesIdsByTraitSystem)
|
|
{
|
|
Tree tree = _traitTrees.LookupByKey(traitTreeId);
|
|
if (tree != null)
|
|
_traitTreesByTraitSystem.Add(traitSystemId, tree);
|
|
}
|
|
|
|
foreach (TraitCurrencySourceRecord traitCurrencySource in CliDB.TraitCurrencySourceStorage.Values)
|
|
_traitCurrencySourcesByCurrency.Add(traitCurrencySource.TraitCurrencyID, traitCurrencySource);
|
|
|
|
foreach (TraitDefinitionEffectPointsRecord traitDefinitionEffectPoints in CliDB.TraitDefinitionEffectPointsStorage.Values)
|
|
_traitDefinitionEffectPointModifiers.Add(traitDefinitionEffectPoints.TraitDefinitionID, traitDefinitionEffectPoints);
|
|
|
|
MultiMap<uint, TraitTreeLoadoutEntryRecord> traitTreeLoadoutEntries = new();
|
|
foreach (TraitTreeLoadoutEntryRecord traitTreeLoadoutEntry in CliDB.TraitTreeLoadoutEntryStorage.Values)
|
|
traitTreeLoadoutEntries[traitTreeLoadoutEntry.TraitTreeLoadoutID].Add(traitTreeLoadoutEntry);
|
|
|
|
foreach (TraitTreeLoadoutRecord traitTreeLoadout in CliDB.TraitTreeLoadoutStorage.Values)
|
|
{
|
|
var entries = traitTreeLoadoutEntries.LookupByKey(traitTreeLoadout.Id);
|
|
if (entries != null)
|
|
{
|
|
entries.Sort((left, right) => { return left.OrderIndex.CompareTo(right.OrderIndex); });
|
|
// there should be only one loadout per spec, we take last one encountered
|
|
_traitTreeLoadoutsByChrSpecialization[traitTreeLoadout.ChrSpecializationID] = entries;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates new TraitConfig identifier.
|
|
* Because this only needs to be unique for each character we let it overflow
|
|
*/
|
|
public static int GenerateNewTraitConfigId()
|
|
{
|
|
if (_configIdGenerator == int.MaxValue)
|
|
_configIdGenerator = 0;
|
|
|
|
return ++_configIdGenerator;
|
|
}
|
|
|
|
public static TraitConfigType GetConfigTypeForTree(int traitTreeId)
|
|
{
|
|
Tree tree = _traitTrees.LookupByKey(traitTreeId);
|
|
if (tree == null)
|
|
return TraitConfigType.Invalid;
|
|
|
|
return tree.ConfigType;
|
|
}
|
|
|
|
/**
|
|
* @brief Finds relevant TraitTree identifiers
|
|
* @param traitConfig config data
|
|
* @return Trait tree data
|
|
*/
|
|
public static List<Tree> GetTreesForConfig(TraitConfigPacket traitConfig)
|
|
{
|
|
switch (traitConfig.Type)
|
|
{
|
|
case TraitConfigType.Combat:
|
|
ChrSpecializationRecord chrSpecializationEntry = CliDB.ChrSpecializationStorage.LookupByKey(traitConfig.ChrSpecializationID);
|
|
if (chrSpecializationEntry != null)
|
|
return _traitTreesBySkillLine.LookupByKey(_skillLinesByClass[chrSpecializationEntry.ClassID]);
|
|
break;
|
|
case TraitConfigType.Profession:
|
|
return _traitTreesBySkillLine.LookupByKey(traitConfig.SkillLineID);
|
|
case TraitConfigType.Generic:
|
|
return _traitTreesByTraitSystem.LookupByKey(traitConfig.TraitSystemID);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static bool HasEnoughCurrency(TraitEntryPacket entry, Dictionary<uint, int> currencies)
|
|
{
|
|
int getCurrencyCount(int currencyId)
|
|
{
|
|
return currencies.LookupByKey(currencyId);
|
|
}
|
|
|
|
Node node = _traitNodes.LookupByKey(entry.TraitNodeID);
|
|
foreach (NodeGroup group in node.Groups)
|
|
foreach (TraitCostRecord cost in group.Costs)
|
|
if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank)
|
|
return false;
|
|
|
|
var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID);
|
|
if (nodeEntryItr != null)
|
|
foreach (TraitCostRecord cost in nodeEntryItr.Costs)
|
|
if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank)
|
|
return false;
|
|
|
|
foreach (TraitCostRecord cost in node.Costs)
|
|
if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank)
|
|
return false;
|
|
|
|
Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID);
|
|
if (tree != null)
|
|
foreach (TraitCostRecord cost in tree.Costs)
|
|
if (getCurrencyCount(cost.TraitCurrencyID) < cost.Amount * entry.Rank)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void TakeCurrencyCost(TraitEntryPacket entry, Dictionary<uint, int> currencies)
|
|
{
|
|
Node node = _traitNodes.LookupByKey(entry.TraitNodeID);
|
|
foreach (NodeGroup group in node.Groups)
|
|
foreach (TraitCostRecord cost in group.Costs)
|
|
currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank;
|
|
|
|
var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID);
|
|
if (nodeEntryItr != null)
|
|
foreach (TraitCostRecord cost in nodeEntryItr.Costs)
|
|
currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank;
|
|
|
|
foreach (TraitCostRecord cost in node.Costs)
|
|
currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank;
|
|
|
|
Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID);
|
|
if (tree != null)
|
|
foreach (TraitCostRecord cost in tree.Costs)
|
|
currencies[(uint)cost.TraitCurrencyID] -= cost.Amount * entry.Rank;
|
|
}
|
|
|
|
public static void FillOwnedCurrenciesMap(TraitConfigPacket traitConfig, Player player, Dictionary<uint, int> currencies)
|
|
{
|
|
List<Tree> trees = GetTreesForConfig(traitConfig);
|
|
if (trees == null)
|
|
return;
|
|
|
|
bool hasTraitNodeEntry(int traitNodeEntryId)
|
|
{
|
|
return traitConfig.Entries.Any(traitEntry => traitEntry.TraitNodeEntryID == traitNodeEntryId && (traitEntry.Rank > 0 || traitEntry.GrantedRanks > 0));
|
|
}
|
|
|
|
foreach (Tree tree in trees)
|
|
{
|
|
foreach (TraitCurrencyRecord currency in tree.Currencies)
|
|
{
|
|
switch (currency.GetCurrencyType())
|
|
{
|
|
case TraitCurrencyType.Gold:
|
|
{
|
|
if (!currencies.ContainsKey((int)currency.Id))
|
|
currencies[currency.Id] = 0;
|
|
|
|
int amount = currencies[currency.Id];
|
|
if (player.GetMoney() > (ulong)(int.MaxValue - amount))
|
|
amount = int.MaxValue;
|
|
else
|
|
amount += (int)player.GetMoney();
|
|
break;
|
|
}
|
|
case TraitCurrencyType.CurrencyTypesBased:
|
|
if (!currencies.ContainsKey((int)currency.Id))
|
|
currencies[currency.Id] = 0;
|
|
|
|
currencies[currency.Id] += (int)player.GetCurrencyQuantity((uint)currency.CurrencyTypesID);
|
|
break;
|
|
case TraitCurrencyType.TraitSourced:
|
|
var currencySources = _traitCurrencySourcesByCurrency.LookupByKey(currency.Id);
|
|
if (currencySources != null)
|
|
{
|
|
foreach (TraitCurrencySourceRecord currencySource in currencySources)
|
|
{
|
|
if (currencySource.QuestID != 0 && !player.IsQuestRewarded(currencySource.QuestID))
|
|
continue;
|
|
|
|
if (currencySource.AchievementID != 0 && !player.HasAchieved(currencySource.AchievementID))
|
|
continue;
|
|
|
|
if (currencySource.PlayerLevel != 0 && player.GetLevel() < currencySource.PlayerLevel)
|
|
continue;
|
|
|
|
if (currencySource.TraitNodeEntryID != 0 && !hasTraitNodeEntry(currencySource.TraitNodeEntryID))
|
|
continue;
|
|
|
|
if (!currencies.ContainsKey(currencySource.TraitCurrencyID))
|
|
currencies[currencySource.TraitCurrencyID] = 0;
|
|
|
|
currencies[currencySource.TraitCurrencyID] += currencySource.Amount;
|
|
}
|
|
}
|
|
break;
|
|
case TraitCurrencyType.TraitSourcedPlayerDataElement:
|
|
if (currency.PlayerDataElementAccountID != 0)
|
|
currencies[currency.Id] += player.GetDataElementAccount((uint)currency.CurrencyTypesID);
|
|
else if (currency.PlayerDataElementCharacterID != 0)
|
|
currencies[currency.Id] += player.GetDataElementCharacter((uint)currency.CurrencyTypesID);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void AddSpentCurrenciesForEntry(TraitEntryPacket entry, Dictionary<int, int> cachedCurrencies, int multiplier)
|
|
{
|
|
Node node = _traitNodes.LookupByKey(entry.TraitNodeID);
|
|
foreach (NodeGroup group in node.Groups)
|
|
{
|
|
foreach (TraitCostRecord cost in group.Costs)
|
|
{
|
|
if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID))
|
|
cachedCurrencies[cost.TraitCurrencyID] = 0;
|
|
|
|
cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier;
|
|
}
|
|
}
|
|
|
|
var nodeEntryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == entry.TraitNodeEntryID);
|
|
if (nodeEntryItr != null)
|
|
{
|
|
foreach (TraitCostRecord cost in nodeEntryItr.Costs)
|
|
{
|
|
if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID))
|
|
cachedCurrencies[cost.TraitCurrencyID] = 0;
|
|
|
|
cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier;
|
|
}
|
|
}
|
|
|
|
foreach (TraitCostRecord cost in node.Costs)
|
|
{
|
|
if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID))
|
|
cachedCurrencies[cost.TraitCurrencyID] = 0;
|
|
|
|
cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier;
|
|
}
|
|
|
|
Tree tree = _traitTrees.LookupByKey(node.Data.TraitTreeID);
|
|
if (tree != null)
|
|
{
|
|
foreach (TraitCostRecord cost in tree.Costs)
|
|
{
|
|
if (!cachedCurrencies.ContainsKey(cost.TraitCurrencyID))
|
|
cachedCurrencies[cost.TraitCurrencyID] = 0;
|
|
|
|
cachedCurrencies[cost.TraitCurrencyID] += cost.Amount * entry.Rank * multiplier;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void FillSpentCurrenciesMap(List<TraitEntryPacket> traitEntries, Dictionary<int, int> cachedCurrencies)
|
|
{
|
|
foreach (TraitEntryPacket entry in traitEntries)
|
|
AddSpentCurrenciesForEntry(entry, cachedCurrencies, 1);
|
|
}
|
|
|
|
public static int[] GetClassAndSpecTreeCurrencies(TraitConfigPacket traitConfig)
|
|
{
|
|
int[] currencies = new int[2];
|
|
|
|
List<Tree> trees = GetTreesForConfig(traitConfig);
|
|
if (trees != null)
|
|
{
|
|
int destIndex = 0;
|
|
foreach (var tree in trees)
|
|
{
|
|
if (destIndex >= currencies.Length)
|
|
break;
|
|
|
|
foreach (var currency in tree.Currencies)
|
|
{
|
|
if (destIndex >= currencies.Length)
|
|
break;
|
|
|
|
currencies[destIndex++] = (int)currency.Id;
|
|
}
|
|
}
|
|
}
|
|
|
|
return currencies;
|
|
}
|
|
|
|
public static List<TraitCurrencyRecord> GetSubTreeCurrency(int traitSubTreeId)
|
|
{
|
|
SubTree subTree = _traitSubTrees.LookupByKey(traitSubTreeId);
|
|
if (subTree == null)
|
|
return null;
|
|
|
|
return subTree.Currencies;
|
|
}
|
|
|
|
public static bool MeetsTraitCondition(TraitConfigPacket traitConfig, Player player, TraitCondRecord condition, ref Dictionary<int, int> cachedCurrencies)
|
|
{
|
|
if (condition.QuestID != 0 && !player.IsQuestRewarded(condition.QuestID))
|
|
return false;
|
|
|
|
if (condition.AchievementID != 0 && !player.HasAchieved(condition.AchievementID))
|
|
return false;
|
|
|
|
if (condition.SpecSetID != 0)
|
|
{
|
|
uint chrSpecializationId = (uint)player.GetPrimarySpecialization();
|
|
if (traitConfig.Type == TraitConfigType.Combat)
|
|
chrSpecializationId = (uint)traitConfig.ChrSpecializationID;
|
|
|
|
if (!Global.DB2Mgr.IsSpecSetMember(condition.SpecSetID, chrSpecializationId))
|
|
return false;
|
|
}
|
|
|
|
if (condition.TraitCurrencyID != 0 && condition.SpentAmountRequired != 0)
|
|
{
|
|
if (cachedCurrencies == null)
|
|
{
|
|
cachedCurrencies = new();
|
|
FillSpentCurrenciesMap(traitConfig.Entries, cachedCurrencies);
|
|
}
|
|
|
|
if (condition.TraitNodeGroupID != 0 || condition.TraitNodeID != 0 || condition.TraitNodeEntryID != 0)
|
|
{
|
|
cachedCurrencies.TryAdd(condition.TraitCurrencyID, 0);
|
|
if (cachedCurrencies[condition.TraitCurrencyID] < condition.SpentAmountRequired)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (condition.RequiredLevel != 0 && player.GetLevel() < condition.RequiredLevel)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static bool NodeMeetsTraitConditions(TraitConfigPacket traitConfig, Node node, uint traitNodeEntryId, Player player, Dictionary<int, int> spentCurrencies)
|
|
{
|
|
var meetsConditions = (bool isSufficient, bool hasFailedConditions) (List<TraitCondRecord> conditions) =>
|
|
{
|
|
bool isSufficient = false;
|
|
bool hasFailedConditions = false;
|
|
|
|
foreach (var condition in conditions)
|
|
{
|
|
if (condition.GetCondType() == TraitConditionType.Available || condition.GetCondType() == TraitConditionType.Visible)
|
|
{
|
|
if (MeetsTraitCondition(traitConfig, player, condition, ref spentCurrencies))
|
|
{
|
|
if (condition.HasFlag(TraitCondFlags.IsSufficient))
|
|
{
|
|
isSufficient = true;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
hasFailedConditions = true;
|
|
}
|
|
}
|
|
|
|
return (isSufficient, hasFailedConditions);
|
|
};
|
|
|
|
bool IsSufficient;
|
|
bool HasFailedConditions;
|
|
|
|
bool hasFailedConditions = false;
|
|
foreach (NodeEntry entry in node.Entries)
|
|
{
|
|
if (entry.Data.Id == traitNodeEntryId)
|
|
{
|
|
(IsSufficient, HasFailedConditions) = meetsConditions(entry.Conditions);
|
|
if (IsSufficient)
|
|
return true;
|
|
if (HasFailedConditions)
|
|
hasFailedConditions = true;
|
|
}
|
|
}
|
|
|
|
(IsSufficient, HasFailedConditions) = meetsConditions(node.Conditions);
|
|
if (IsSufficient)
|
|
return true;
|
|
else if (HasFailedConditions)
|
|
hasFailedConditions = true;
|
|
|
|
foreach (NodeGroup group in node.Groups)
|
|
{
|
|
(IsSufficient, HasFailedConditions) = meetsConditions(group.Conditions);
|
|
if (IsSufficient)
|
|
return true;
|
|
if (HasFailedConditions)
|
|
hasFailedConditions = true;
|
|
}
|
|
|
|
return !hasFailedConditions;
|
|
}
|
|
|
|
public static List<TraitEntry> GetGrantedTraitEntriesForConfig(TraitConfigPacket traitConfig, Player player)
|
|
{
|
|
List<TraitEntry> entries = new();
|
|
var trees = GetTreesForConfig(traitConfig);
|
|
if (trees == null)
|
|
return entries;
|
|
|
|
void addGrantedRankToEntry(uint nodeId, NodeEntry entry, int grantedRanks)
|
|
{
|
|
TraitEntry foundTraitEntry = entries.Find(traitEntry => traitEntry.TraitNodeID == nodeId && traitEntry.TraitNodeEntryID == entry.Data.Id);
|
|
if (foundTraitEntry == null)
|
|
{
|
|
foundTraitEntry = new();
|
|
foundTraitEntry.TraitNodeID = (int)nodeId;
|
|
foundTraitEntry.TraitNodeEntryID = (int)entry.Data.Id;
|
|
foundTraitEntry.Rank = 0;
|
|
foundTraitEntry.GrantedRanks = 0;
|
|
entries.Add(foundTraitEntry);
|
|
}
|
|
foundTraitEntry.GrantedRanks += grantedRanks;
|
|
if (foundTraitEntry.GrantedRanks > entry.Data.MaxRanks)
|
|
foundTraitEntry.GrantedRanks = entry.Data.MaxRanks;
|
|
}
|
|
|
|
Dictionary<int, int> cachedCurrencies = null;
|
|
|
|
foreach (Tree tree in trees)
|
|
{
|
|
foreach (Node node in tree.Nodes)
|
|
{
|
|
foreach (NodeEntry entry in node.Entries)
|
|
foreach (TraitCondRecord condition in entry.Conditions)
|
|
if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies))
|
|
addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks);
|
|
|
|
foreach (TraitCondRecord condition in node.Conditions)
|
|
if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies))
|
|
foreach (NodeEntry entry in node.Entries)
|
|
addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks);
|
|
|
|
foreach (NodeGroup group in node.Groups)
|
|
foreach (TraitCondRecord condition in group.Conditions)
|
|
if (condition.GetCondType() == TraitConditionType.Granted && MeetsTraitCondition(traitConfig, player, condition, ref cachedCurrencies))
|
|
foreach (NodeEntry entry in node.Entries)
|
|
addGrantedRankToEntry(node.Data.Id, entry, condition.GrantedRanks);
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
public static bool IsValidEntry(TraitEntryPacket traitEntry)
|
|
{
|
|
Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID);
|
|
if (node == null)
|
|
return false;
|
|
|
|
var entryItr = node.Entries.Find(entry => entry.Data.Id == traitEntry.TraitNodeEntryID);
|
|
if (entryItr == null)
|
|
return false;
|
|
|
|
if (entryItr.Data.MaxRanks < traitEntry.Rank + traitEntry.GrantedRanks)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static TalentLearnResult ValidateConfig(TraitConfigPacket traitConfig, Player player, bool requireSpendingAllCurrencies = false, bool removeInvalidEntries = false)
|
|
{
|
|
int getNodeEntryCount(int traitNodeId)
|
|
{
|
|
return traitConfig.Entries.Count(traitEntry => traitEntry.TraitNodeID == traitNodeId);
|
|
}
|
|
|
|
TraitEntryPacket getNodeEntry(uint traitNodeId, uint traitNodeEntryId)
|
|
{
|
|
return traitConfig.Entries.Find(traitEntry => traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId);
|
|
}
|
|
|
|
bool isNodeFullyFilled(Node node)
|
|
{
|
|
bool nodeEntryMatches(NodeEntry nodeEntry)
|
|
{
|
|
TraitEntryPacket traitEntry = getNodeEntry(node.Data.Id, nodeEntry.Data.Id);
|
|
return traitEntry != null && (traitEntry.Rank + traitEntry.GrantedRanks) == nodeEntry.Data.MaxRanks;
|
|
};
|
|
|
|
if (node.Data.GetNodeType() == TraitNodeType.Selection)
|
|
return node.Entries.Any(nodeEntryMatches);
|
|
|
|
return node.Entries.All(nodeEntryMatches);
|
|
}
|
|
|
|
Dictionary<int, int> spentCurrencies = new();
|
|
FillSpentCurrenciesMap(traitConfig.Entries, spentCurrencies);
|
|
|
|
var isValidTraitEntry = TalentLearnResult (TraitEntryPacket traitEntry) =>
|
|
{
|
|
if (!IsValidEntry(traitEntry))
|
|
return TalentLearnResult.FailedUnknown;
|
|
|
|
Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID);
|
|
if (node.Data.GetNodeType() == TraitNodeType.Selection || node.Data.GetNodeType() == TraitNodeType.SubTreeSelection)
|
|
if (getNodeEntryCount(traitEntry.TraitNodeID) != 1)
|
|
return TalentLearnResult.FailedUnknown;
|
|
|
|
if (!NodeMeetsTraitConditions(traitConfig, node, (uint)traitEntry.TraitNodeEntryID, player, spentCurrencies))
|
|
return TalentLearnResult.FailedUnknown;
|
|
|
|
if (!node.ParentNodes.Empty())
|
|
{
|
|
bool hasAnyParentTrait = false;
|
|
foreach (var (parentNode, edgeType) in node.ParentNodes)
|
|
{
|
|
if (!isNodeFullyFilled(parentNode))
|
|
{
|
|
if (edgeType == TraitEdgeType.RequiredForAvailability)
|
|
return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree;
|
|
|
|
continue;
|
|
}
|
|
|
|
hasAnyParentTrait = true;
|
|
}
|
|
|
|
if (!hasAnyParentTrait)
|
|
return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree;
|
|
}
|
|
|
|
return TalentLearnResult.LearnOk;
|
|
};
|
|
|
|
for (var i = 0; i != traitConfig.Entries.Count;)
|
|
{
|
|
TalentLearnResult result = isValidTraitEntry(traitConfig.Entries[i]);
|
|
if (result != TalentLearnResult.LearnOk)
|
|
{
|
|
if (!removeInvalidEntries)
|
|
return result;
|
|
|
|
AddSpentCurrenciesForEntry(traitConfig.Entries[i], spentCurrencies, -1);
|
|
|
|
if (traitConfig.Entries[i].GrantedRanks == 0 // fully remove entries that don't have granted ranks
|
|
|| traitConfig.Entries[i].Rank == 0) // ... or entries that do have them and don't have any additional spent ranks (can happen if the same entry is revalidated after first removing all spent ranks)
|
|
traitConfig.Entries.RemoveAt(i);
|
|
else
|
|
traitConfig.Entries[i].Rank = 0;
|
|
|
|
// revalidate entire config - a removed entry will invalidate all other entries that depend on it
|
|
i = 0;
|
|
}
|
|
else
|
|
++i;
|
|
}
|
|
|
|
Dictionary<int, SubtreeValidationData> subtrees = new();
|
|
|
|
foreach (TraitEntryPacket traitEntry in traitConfig.Entries)
|
|
{
|
|
Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID);
|
|
var entryItr = node.Entries.Find(nodeEntry => nodeEntry.Data.Id == traitEntry.TraitNodeEntryID);
|
|
Cypher.Assert(entryItr != null);
|
|
|
|
if (!subtrees.ContainsKey(entryItr.Data.TraitSubTreeID))
|
|
subtrees[entryItr.Data.TraitSubTreeID] = new();
|
|
|
|
if (node.Data.GetNodeType() == TraitNodeType.SubTreeSelection)
|
|
subtrees[entryItr.Data.TraitSubTreeID].IsSelected = true;
|
|
|
|
if (node.Data.TraitSubTreeID != 0)
|
|
subtrees[node.Data.TraitSubTreeID].Entries.Add(traitEntry);
|
|
}
|
|
|
|
foreach (TraitSubTreeCachePacket subTree in traitConfig.SubTrees)
|
|
subTree.Active = false;
|
|
|
|
foreach (var (selectedSubTreeId, data) in subtrees)
|
|
{
|
|
var subtreeDataItr = traitConfig.SubTrees.Find(p => p.TraitSubTreeID == selectedSubTreeId);
|
|
if (subtreeDataItr == null)
|
|
{
|
|
subtreeDataItr = new TraitSubTreeCachePacket();
|
|
subtreeDataItr.TraitSubTreeID = selectedSubTreeId;
|
|
traitConfig.SubTrees.Add(subtreeDataItr);
|
|
}
|
|
|
|
subtreeDataItr.Entries = data.Entries;
|
|
subtreeDataItr.Active = data.IsSelected;
|
|
}
|
|
|
|
Dictionary<uint, int> grantedCurrencies = new();
|
|
FillOwnedCurrenciesMap(traitConfig, player, grantedCurrencies);
|
|
|
|
foreach (var (traitCurrencyId, spentAmount) in spentCurrencies)
|
|
{
|
|
if (CliDB.TraitCurrencyStorage.LookupByKey(traitCurrencyId).GetCurrencyType() != TraitCurrencyType.TraitSourced)
|
|
continue;
|
|
|
|
if (spentAmount == 0)
|
|
continue;
|
|
|
|
int grantedCount = grantedCurrencies.LookupByKey(traitCurrencyId);
|
|
if (grantedCount == 0 || grantedCount < spentAmount)
|
|
return TalentLearnResult.FailedNotEnoughTalentsInPrimaryTree;
|
|
|
|
}
|
|
|
|
if (requireSpendingAllCurrencies && traitConfig.Type == TraitConfigType.Combat)
|
|
{
|
|
// client checks only first two currencies for trait tree
|
|
foreach (int traitCurrencyId in GetClassAndSpecTreeCurrencies(traitConfig))
|
|
{
|
|
int grantedAmount = grantedCurrencies.LookupByKey(traitCurrencyId);
|
|
if (grantedAmount == 0)
|
|
continue;
|
|
|
|
int spentAmount = spentCurrencies.LookupByKey(traitCurrencyId);
|
|
if (spentAmount == 0 || spentAmount != grantedAmount)
|
|
return TalentLearnResult.UnspentTalentPoints;
|
|
}
|
|
|
|
foreach (var (selectedTraitSubTreeId, data) in subtrees)
|
|
{
|
|
if (!data.IsSelected)
|
|
continue;
|
|
|
|
foreach (TraitCurrencyRecord subTreeCurrency in GetSubTreeCurrency(selectedTraitSubTreeId))
|
|
{
|
|
int grantedAmount = grantedCurrencies.LookupByKey(subTreeCurrency.Id);
|
|
if (grantedAmount == 0)
|
|
continue;
|
|
|
|
int spentAmount = spentCurrencies.LookupByKey(subTreeCurrency.Id);
|
|
if (spentAmount == 0 || spentAmount != grantedAmount)
|
|
return TalentLearnResult.UnspentTalentPoints;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TalentLearnResult.LearnOk;
|
|
}
|
|
|
|
public static bool CanApplyTraitNode(TraitConfig traitConfig, TraitEntry traitEntry)
|
|
{
|
|
Node node = _traitNodes.LookupByKey(traitEntry.TraitNodeID);
|
|
if (node == null)
|
|
return false;
|
|
|
|
if (node.Data.TraitSubTreeID != 0)
|
|
{
|
|
var subTreeItr = traitConfig.SubTrees._values.Find(p => p.TraitSubTreeID == node.Data.TraitSubTreeID);
|
|
if (subTreeItr == null || subTreeItr.Active == 0)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static List<TraitDefinitionEffectPointsRecord> GetTraitDefinitionEffectPointModifiers(int traitDefinitionId)
|
|
{
|
|
return _traitDefinitionEffectPointModifiers.LookupByKey(traitDefinitionId);
|
|
}
|
|
|
|
public static void InitializeStarterBuildTraitConfig(TraitConfigPacket traitConfig, Player player)
|
|
{
|
|
traitConfig.Entries.Clear();
|
|
var trees = GetTreesForConfig(traitConfig);
|
|
if (trees == null)
|
|
return;
|
|
|
|
foreach (TraitEntry grant in GetGrantedTraitEntriesForConfig(traitConfig, player))
|
|
{
|
|
TraitEntryPacket newEntry = new();
|
|
newEntry.TraitNodeID = grant.TraitNodeID;
|
|
newEntry.TraitNodeEntryID = grant.TraitNodeEntryID;
|
|
newEntry.GrantedRanks = grant.GrantedRanks;
|
|
traitConfig.Entries.Add(newEntry);
|
|
}
|
|
|
|
Dictionary<uint, int> currencies = new();
|
|
FillOwnedCurrenciesMap(traitConfig, player, currencies);
|
|
|
|
var loadoutEntries = _traitTreeLoadoutsByChrSpecialization.LookupByKey(traitConfig.ChrSpecializationID);
|
|
if (loadoutEntries != null)
|
|
{
|
|
TraitEntryPacket findEntry(TraitConfigPacket config, int traitNodeId, int traitNodeEntryId)
|
|
{
|
|
return config.Entries.Find(traitEntry => traitEntry.TraitNodeID == traitNodeId && traitEntry.TraitNodeEntryID == traitNodeEntryId);
|
|
}
|
|
|
|
foreach (TraitTreeLoadoutEntryRecord loadoutEntry in loadoutEntries)
|
|
{
|
|
int addedRanks = loadoutEntry.NumPoints;
|
|
Node node = _traitNodes.LookupByKey(loadoutEntry.SelectedTraitNodeID);
|
|
|
|
TraitEntryPacket newEntry = new();
|
|
newEntry.TraitNodeID = loadoutEntry.SelectedTraitNodeID;
|
|
newEntry.TraitNodeEntryID = loadoutEntry.SelectedTraitNodeEntryID;
|
|
if (newEntry.TraitNodeEntryID == 0)
|
|
newEntry.TraitNodeEntryID = (int)node.Entries[0].Data.Id;
|
|
|
|
TraitEntryPacket entryInConfig = findEntry(traitConfig, newEntry.TraitNodeID, newEntry.TraitNodeEntryID);
|
|
|
|
if (entryInConfig != null)
|
|
addedRanks -= entryInConfig.Rank;
|
|
|
|
newEntry.Rank = addedRanks;
|
|
|
|
if (!HasEnoughCurrency(newEntry, currencies))
|
|
continue;
|
|
|
|
if (entryInConfig != null)
|
|
entryInConfig.Rank += addedRanks;
|
|
else
|
|
traitConfig.Entries.Add(newEntry);
|
|
|
|
TakeCurrencyCost(newEntry, currencies);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class NodeEntry
|
|
{
|
|
public TraitNodeEntryRecord Data;
|
|
public List<TraitCondRecord> Conditions = new();
|
|
public List<TraitCostRecord> Costs = new();
|
|
}
|
|
|
|
class Node
|
|
{
|
|
public TraitNodeRecord Data;
|
|
public List<NodeEntry> Entries = new();
|
|
public List<NodeGroup> Groups = new();
|
|
public List<Tuple<Node, TraitEdgeType>> ParentNodes = new(); // TraitEdge::LeftTraitNodeID
|
|
public List<TraitCondRecord> Conditions = new();
|
|
public List<TraitCostRecord> Costs = new();
|
|
}
|
|
|
|
class NodeGroup
|
|
{
|
|
public TraitNodeGroupRecord Data;
|
|
public List<TraitCondRecord> Conditions = new();
|
|
public List<TraitCostRecord> Costs = new();
|
|
public List<Node> Nodes = new();
|
|
}
|
|
|
|
class SubTree
|
|
{
|
|
public TraitSubTreeRecord Data;
|
|
public List<Node> Nodes = new();
|
|
public List<TraitCurrencyRecord> Currencies = new();
|
|
}
|
|
|
|
class Tree
|
|
{
|
|
public TraitTreeRecord Data;
|
|
public List<Node> Nodes = new();
|
|
public List<TraitCostRecord> Costs = new();
|
|
public List<TraitCurrencyRecord> Currencies = new();
|
|
public List<SubTree> SubTrees = new();
|
|
public TraitConfigType ConfigType;
|
|
}
|
|
|
|
class SubtreeValidationData
|
|
{
|
|
public List<TraitEntryPacket> Entries = new();
|
|
public bool IsSelected;
|
|
}
|
|
}
|