Client LuaCsForBarotrauma
TalentTree.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using System.Linq;
5 
6 namespace Barotrauma
7 {
8  internal sealed class TalentTree : Prefab
9  {
10  public enum TalentStages
11  {
12  Invalid,
13  Locked,
14  Unlocked,
15  Available,
16  Highlighted
17  }
18 
19  public static readonly PrefabCollection<TalentTree> JobTalentTrees = new PrefabCollection<TalentTree>();
20 
21  public readonly ImmutableArray<TalentSubTree> TalentSubTrees;
22 
26  public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
27 
28  public ContentXElement ConfigElement
29  {
30  get;
31  private set;
32  }
33 
34  public TalentTree(ContentXElement element, TalentTreesFile file) : base(file, element.GetAttributeIdentifier("jobIdentifier", ""))
35  {
36  ConfigElement = element;
37 
38  if (Identifier.IsEmpty)
39  {
40  DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!",
41  contentPackage: element.ContentPackage);
42  return;
43  }
44 
45  List<TalentSubTree> subTrees = new List<TalentSubTree>();
46  foreach (var subTreeElement in element.GetChildElements("subtree"))
47  {
48  subTrees.Add(new TalentSubTree(subTreeElement));
49  }
50 
51  TalentSubTrees = subTrees.ToImmutableArray();
52  AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet();
53  }
54 
55  public bool TalentIsInTree(Identifier talentIdentifier)
56  {
57  return AllTalentIdentifiers.Contains(talentIdentifier);
58  }
59 
60  public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier)
61  {
62  return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection<Identifier>)Array.Empty<Identifier>());
63  }
64 
65  public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection<Identifier> selectedTalents)
66  {
67  IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
68  requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
69 
70  return requiredSubTrees.All(tst => tst.HasEnoughTalents(selectedTalents)) && // check if we meet requirements
71  !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents) && !tst.HasMaxTalents(selectedTalents)); // check if any other talent trees are blocking this one
72  }
73 
74  // i hate this function - markus
75  // me too - joonas
76  public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection<Identifier> selectedTalents)
77  {
78  if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; }
79 
80  if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; }
81 
82  TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
83  if (subTree is null) { return TalentStages.Invalid; }
84 
85  TalentOption targetTalentOption = subTree.TalentOptionStages[index];
86  if (targetTalentOption.HasEnoughTalents(character.Info))
87  {
88  return TalentStages.Unlocked;
89  }
90 
91  if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
92  {
93  return TalentStages.Locked;
94  }
95 
96  if (targetTalentOption.HasSelectedTalent(selectedTalents))
97  {
98  return TalentStages.Highlighted;
99  }
100 
101  bool hasTalentInLastTier = true;
102  bool isLastTalentPurchased = true;
103 
104  int lastindex = index - 1;
105  if (lastindex >= 0)
106  {
107  TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex];
108  hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents);
109  isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info);
110  }
111 
112  if (!hasTalentInLastTier)
113  {
114  return TalentStages.Locked;
115  }
116 
117  bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0;
118 
119  if (hasPointsForNewTalent)
120  {
121  return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available;
122  }
123 
124  return TalentStages.Locked;
125  }
126 
127 
128  public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
129  {
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; }
133 
134  if (IsTalentLocked(talentIdentifier)) { return false; }
135 
136  if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
137  {
138  //if the character already has the talent, it must be viable?
139  //needed for backwards compatibility, otherwise if we remove e.g. a tier 1 or tier 2 talent,
140  //all the already-unlocked higher-tier talents will be considered invalid which'll break the talent selection
141  return true;
142  }
143 
144  foreach (var subTree in talentTree!.TalentSubTrees)
145  {
146  if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
147 
148  foreach (var talentOptionStage in subTree.TalentOptionStages)
149  {
150  if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
151  {
152  return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
153  }
154  //if a previous stage hasn't been completed, this talent can't be selected yet
155  bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
156  if (!optionStageCompleted)
157  {
158  break;
159  }
160  }
161  }
162 
163  return false;
164  }
165 
166  public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
167  {
168  characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
169 
170  foreach (Character c in characterList)
171  {
172  if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
173  }
174 
175  return false;
176  }
177 
178  public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
179  {
180  List<Identifier> viableTalents = new List<Identifier>();
181  bool canStillUnlock = true;
182  // keep trying to unlock talents until none of the talents are unlockable
183  while (canStillUnlock && selectedTalents.Any())
184  {
185  canStillUnlock = false;
186  foreach (Identifier talent in selectedTalents)
187  {
188  if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
189  {
190  viableTalents.Add(talent);
191  canStillUnlock = true;
192  }
193  }
194  }
195 
196  return viableTalents;
197  }
198 
199  public override void Dispose() { }
200  }
201 
202  internal enum TalentTreeType
203  {
204  Specialization,
205  Primary
206  }
207 
208  internal sealed class TalentSubTree
209  {
210  public Identifier Identifier { get; }
211 
212  public LocalizedString DisplayName { get; }
213 
214  public readonly ImmutableArray<TalentOption> TalentOptionStages;
215 
216  public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
217 
218  public readonly TalentTreeType Type;
219  public readonly ImmutableHashSet<Identifier> RequiredTrees;
220  public readonly ImmutableHashSet<Identifier> BlockedTrees;
221 
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));
225 
226  public TalentSubTree(ContentXElement subTreeElement)
227  {
228  Identifier = subTreeElement.GetAttributeIdentifier("identifier", "");
229  string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty);
230  if (string.IsNullOrWhiteSpace(nameIdentifier))
231  {
232  nameIdentifier = $"talenttree.{Identifier}";
233  }
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"))
240  {
241  talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier));
242  }
243 
244  TalentOptionStages = talentOptionStages.ToImmutableArray();
245  AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
246  }
247  }
248 
249  internal readonly struct TalentOption
250  {
251  private readonly ImmutableHashSet<Identifier> talentIdentifiers;
252 
253  public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
254 
258  public readonly int RequiredTalents;
262  public readonly int MaxChosenTalents;
263 
268  public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
269 
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;
273 
274  // No LINQ
275  public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
276  {
277  foreach (Identifier talent in selectedTalents)
278  {
279  if (talentIdentifiers.Contains(talent))
280  {
281  return true;
282  }
283  }
284  return false;
285  }
286 
287  public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
288  {
289  int i = 0;
290  foreach (Identifier talent in talents)
291  {
292  if (talentIdentifiers.Contains(talent))
293  {
294  i++;
295  }
296  }
297  return i;
298  }
299 
300  public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
301  {
302  MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
303  RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
304 
305  if (RequiredTalents > MaxChosenTalents)
306  {
307  DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
308  contentPackage: talentOptionsElement.ContentPackage);
309  }
310 
311  HashSet<Identifier> identifiers = new HashSet<Identifier>();
312  foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
313  {
314  Identifier elementName = talentOptionElement.Name.ToIdentifier();
315  if (elementName == "talentoption")
316  {
317  identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty));
318  }
319  else if (elementName == "showcasetalent")
320  {
321  Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
322  HashSet<Identifier> showCaseTalentIdentifiers = new HashSet<Identifier>();
323  foreach (ContentXElement subElement in talentOptionElement.Elements())
324  {
325  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
326  showCaseTalentIdentifiers.Add(identifier);
327  identifiers.Add(identifier);
328  }
329  ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
330  }
331  }
332 
333  talentIdentifiers = identifiers.ToImmutableHashSet();
334 
335  if (RequiredTalents > talentIdentifiers.Count)
336  {
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);
339  }
340  if (MaxChosenTalents > talentIdentifiers.Count)
341  {
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);
344  }
345  }
346  }
347 }
readonly Identifier Identifier
Definition: Prefab.cs:34
CharacterType
Definition: Enums.cs:685
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180