2 using System.Collections.Generic;
3 using System.Collections.Immutable;
8 internal sealed
class TalentTree : Prefab
10 public enum TalentStages
19 public static readonly PrefabCollection<TalentTree> JobTalentTrees =
new PrefabCollection<TalentTree>();
21 public readonly ImmutableArray<TalentSubTree> TalentSubTrees;
26 public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
28 public ContentXElement ConfigElement
34 public TalentTree(ContentXElement element, TalentTreesFile file) : base(file, element.GetAttributeIdentifier(
"jobIdentifier",
""))
36 ConfigElement = element;
40 DebugConsole.ThrowError($
"No job defined for talent tree in \"{file.Path}\"!",
41 contentPackage: element.ContentPackage);
45 List<TalentSubTree> subTrees =
new List<TalentSubTree>();
46 foreach (var subTreeElement
in element.GetChildElements(
"subtree"))
48 subTrees.Add(
new TalentSubTree(subTreeElement));
51 TalentSubTrees = subTrees.ToImmutableArray();
52 AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet();
55 public bool TalentIsInTree(Identifier talentIdentifier)
57 return AllTalentIdentifiers.Contains(talentIdentifier);
60 public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier)
62 return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection<Identifier>)Array.Empty<Identifier>());
65 public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection<Identifier> selectedTalents)
67 IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
68 requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
70 return requiredSubTrees.All(tst => tst.HasEnoughTalents(selectedTalents)) &&
71 !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents) && !tst.HasMaxTalents(selectedTalents));
76 public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier,
int index, IReadOnlyCollection<Identifier> selectedTalents)
78 if (character?.Info?.Job.Prefab is
null) {
return TalentStages.Invalid; }
80 if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) {
return TalentStages.Invalid; }
82 TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
83 if (subTree is
null) {
return TalentStages.Invalid; }
85 TalentOption targetTalentOption = subTree.TalentOptionStages[index];
86 if (targetTalentOption.HasEnoughTalents(character.Info))
88 return TalentStages.Unlocked;
91 if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
93 return TalentStages.Locked;
96 if (targetTalentOption.HasSelectedTalent(selectedTalents))
98 return TalentStages.Highlighted;
101 bool hasTalentInLastTier =
true;
102 bool isLastTalentPurchased =
true;
104 int lastindex = index - 1;
107 TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex];
108 hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents);
109 isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info);
112 if (!hasTalentInLastTier)
114 return TalentStages.Locked;
117 bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0;
119 if (hasPointsForNewTalent)
121 return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available;
124 return TalentStages.Locked;
128 public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
130 if (character?.Info?.Job.Prefab ==
null) {
return false; }
131 if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) {
return false; }
132 if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) {
return false; }
134 if (IsTalentLocked(talentIdentifier,
Character.GetFriendlyCrew(character))) {
return false; }
136 if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
144 foreach (var subTree
in talentTree!.TalentSubTrees)
146 if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) {
return false; }
148 foreach (var talentOptionStage
in subTree.TalentOptionStages)
150 if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
152 return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
155 bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
156 if (!optionStageCompleted)
166 public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
168 foreach (Character c
in characterList)
170 if (c.Info.GetSavedStatValue(
StatTypes.LockedTalents, talentIdentifier) >= 1) {
return true; }
176 public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
178 List<Identifier> viableTalents =
new List<Identifier>();
179 bool canStillUnlock =
true;
181 while (canStillUnlock && selectedTalents.Any())
183 canStillUnlock =
false;
184 foreach (Identifier talent
in selectedTalents)
186 if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
188 viableTalents.Add(talent);
189 canStillUnlock =
true;
194 return viableTalents;
197 public override void Dispose() { }
200 internal enum TalentTreeType
206 internal sealed
class TalentSubTree
208 public Identifier Identifier {
get; }
210 public LocalizedString DisplayName {
get; }
212 public readonly ImmutableArray<TalentOption> TalentOptionStages;
214 public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
216 public readonly TalentTreeType Type;
217 public readonly ImmutableHashSet<Identifier> RequiredTrees;
218 public readonly ImmutableHashSet<Identifier> BlockedTrees;
220 public bool HasEnoughTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
221 public bool HasMaxTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasMaxTalents(talents));
222 public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
224 public TalentSubTree(ContentXElement subTreeElement)
226 Identifier = subTreeElement.GetAttributeIdentifier(
"identifier",
"");
227 string nameIdentifier = subTreeElement.GetAttributeString(
"nameidentifier",
string.Empty);
228 if (
string.IsNullOrWhiteSpace(nameIdentifier))
230 nameIdentifier = $
"talenttree.{Identifier}";
232 DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
233 Type = subTreeElement.GetAttributeEnum(
"type", TalentTreeType.Specialization);
234 RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet(
"requires", ImmutableHashSet<Identifier>.Empty);
235 BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet(
"blocks", ImmutableHashSet<Identifier>.Empty);
236 List<TalentOption> talentOptionStages =
new List<TalentOption>();
237 foreach (var talentOptionsElement
in subTreeElement.GetChildElements(
"talentoptions"))
239 talentOptionStages.Add(
new TalentOption(talentOptionsElement, Identifier));
242 TalentOptionStages = talentOptionStages.ToImmutableArray();
243 AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
247 internal readonly
struct TalentOption
249 private readonly ImmutableHashSet<Identifier> talentIdentifiers;
251 public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
256 public readonly
int RequiredTalents;
260 public readonly
int MaxChosenTalents;
266 public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents =
new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
268 public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= RequiredTalents;
269 public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= RequiredTalents;
270 public bool HasMaxTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
273 public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
275 foreach (Identifier talent
in selectedTalents)
277 if (talentIdentifiers.Contains(talent))
285 public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
288 foreach (Identifier talent
in talents)
290 if (talentIdentifiers.Contains(talent))
298 public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
300 MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
301 RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
303 if (RequiredTalents > MaxChosenTalents)
305 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
306 contentPackage: talentOptionsElement.ContentPackage);
309 HashSet<Identifier> identifiers =
new HashSet<Identifier>();
310 foreach (ContentXElement talentOptionElement
in talentOptionsElement.Elements())
312 Identifier elementName = talentOptionElement.Name.ToIdentifier();
313 if (elementName ==
"talentoption")
315 identifiers.Add(talentOptionElement.GetAttributeIdentifier(
"identifier", Identifier.Empty));
317 else if (elementName ==
"showcasetalent")
319 Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier(
"identifier", Identifier.Empty);
320 HashSet<Identifier> showCaseTalentIdentifiers =
new HashSet<Identifier>();
321 foreach (ContentXElement subElement
in talentOptionElement.Elements())
323 Identifier identifier = subElement.GetAttributeIdentifier(
"identifier", Identifier.Empty);
324 showCaseTalentIdentifiers.Add(identifier);
325 identifiers.Add(identifier);
327 ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
331 talentIdentifiers = identifiers.ToImmutableHashSet();
333 if (RequiredTalents > talentIdentifiers.Count)
335 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.",
336 contentPackage: talentOptionsElement.ContentPackage);
338 if (MaxChosenTalents > talentIdentifiers.Count)
340 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.",
341 contentPackage: talentOptionsElement.ContentPackage);
readonly Identifier Identifier
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
@ Character
Characters only