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)) {
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, ImmutableHashSet<Character> characterList =
null)
168 characterList ??= GameSession.GetSessionCrewCharacters(
CharacterType.Both);
170 foreach (Character c
in characterList)
172 if (c.Info.GetSavedStatValue(
StatTypes.LockedTalents, talentIdentifier) >= 1) {
return true; }
178 public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
180 List<Identifier> viableTalents =
new List<Identifier>();
181 bool canStillUnlock =
true;
183 while (canStillUnlock && selectedTalents.Any())
185 canStillUnlock =
false;
186 foreach (Identifier talent
in selectedTalents)
188 if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
190 viableTalents.Add(talent);
191 canStillUnlock =
true;
196 return viableTalents;
199 public override void Dispose() { }
202 internal enum TalentTreeType
208 internal sealed
class TalentSubTree
210 public Identifier Identifier {
get; }
212 public LocalizedString DisplayName {
get; }
214 public readonly ImmutableArray<TalentOption> TalentOptionStages;
216 public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
218 public readonly TalentTreeType Type;
219 public readonly ImmutableHashSet<Identifier> RequiredTrees;
220 public readonly ImmutableHashSet<Identifier> BlockedTrees;
222 public bool HasEnoughTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
223 public bool HasMaxTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasMaxTalents(talents));
224 public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
226 public TalentSubTree(ContentXElement subTreeElement)
228 Identifier = subTreeElement.GetAttributeIdentifier(
"identifier",
"");
229 string nameIdentifier = subTreeElement.GetAttributeString(
"nameidentifier",
string.Empty);
230 if (
string.IsNullOrWhiteSpace(nameIdentifier))
232 nameIdentifier = $
"talenttree.{Identifier}";
234 DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
235 Type = subTreeElement.GetAttributeEnum(
"type", TalentTreeType.Specialization);
236 RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet(
"requires", ImmutableHashSet<Identifier>.Empty);
237 BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet(
"blocks", ImmutableHashSet<Identifier>.Empty);
238 List<TalentOption> talentOptionStages =
new List<TalentOption>();
239 foreach (var talentOptionsElement
in subTreeElement.GetChildElements(
"talentoptions"))
241 talentOptionStages.Add(
new TalentOption(talentOptionsElement, Identifier));
244 TalentOptionStages = talentOptionStages.ToImmutableArray();
245 AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
249 internal readonly
struct TalentOption
251 private readonly ImmutableHashSet<Identifier> talentIdentifiers;
253 public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
258 public readonly
int RequiredTalents;
262 public readonly
int MaxChosenTalents;
268 public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents =
new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
270 public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= RequiredTalents;
271 public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= RequiredTalents;
272 public bool HasMaxTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
275 public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
277 foreach (Identifier talent
in selectedTalents)
279 if (talentIdentifiers.Contains(talent))
287 public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
290 foreach (Identifier talent
in talents)
292 if (talentIdentifiers.Contains(talent))
300 public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
302 MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
303 RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
305 if (RequiredTalents > MaxChosenTalents)
307 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
308 contentPackage: talentOptionsElement.ContentPackage);
311 HashSet<Identifier> identifiers =
new HashSet<Identifier>();
312 foreach (ContentXElement talentOptionElement
in talentOptionsElement.Elements())
314 Identifier elementName = talentOptionElement.Name.ToIdentifier();
315 if (elementName ==
"talentoption")
317 identifiers.Add(talentOptionElement.GetAttributeIdentifier(
"identifier", Identifier.Empty));
319 else if (elementName ==
"showcasetalent")
321 Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier(
"identifier", Identifier.Empty);
322 HashSet<Identifier> showCaseTalentIdentifiers =
new HashSet<Identifier>();
323 foreach (ContentXElement subElement
in talentOptionElement.Elements())
325 Identifier identifier = subElement.GetAttributeIdentifier(
"identifier", Identifier.Empty);
326 showCaseTalentIdentifiers.Add(identifier);
327 identifiers.Add(identifier);
329 ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
333 talentIdentifiers = identifiers.ToImmutableHashSet();
335 if (RequiredTalents > talentIdentifiers.Count)
337 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.",
338 contentPackage: talentOptionsElement.ContentPackage);
340 if (MaxChosenTalents > talentIdentifiers.Count)
342 DebugConsole.ThrowError($
"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.",
343 contentPackage: talentOptionsElement.ContentPackage);
readonly Identifier Identifier
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.