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, Character.GetFriendlyCrew(character))) { 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, IEnumerable<Character> characterList)
167  {
168  foreach (Character c in characterList)
169  {
170  if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
171  }
172 
173  return false;
174  }
175 
176  public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
177  {
178  List<Identifier> viableTalents = new List<Identifier>();
179  bool canStillUnlock = true;
180  // keep trying to unlock talents until none of the talents are unlockable
181  while (canStillUnlock && selectedTalents.Any())
182  {
183  canStillUnlock = false;
184  foreach (Identifier talent in selectedTalents)
185  {
186  if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
187  {
188  viableTalents.Add(talent);
189  canStillUnlock = true;
190  }
191  }
192  }
193 
194  return viableTalents;
195  }
196 
197  public override void Dispose() { }
198  }
199 
200  internal enum TalentTreeType
201  {
202  Specialization,
203  Primary
204  }
205 
206  internal sealed class TalentSubTree
207  {
208  public Identifier Identifier { get; }
209 
210  public LocalizedString DisplayName { get; }
211 
212  public readonly ImmutableArray<TalentOption> TalentOptionStages;
213 
214  public readonly ImmutableHashSet<Identifier> AllTalentIdentifiers;
215 
216  public readonly TalentTreeType Type;
217  public readonly ImmutableHashSet<Identifier> RequiredTrees;
218  public readonly ImmutableHashSet<Identifier> BlockedTrees;
219 
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));
223 
224  public TalentSubTree(ContentXElement subTreeElement)
225  {
226  Identifier = subTreeElement.GetAttributeIdentifier("identifier", "");
227  string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty);
228  if (string.IsNullOrWhiteSpace(nameIdentifier))
229  {
230  nameIdentifier = $"talenttree.{Identifier}";
231  }
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"))
238  {
239  talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier));
240  }
241 
242  TalentOptionStages = talentOptionStages.ToImmutableArray();
243  AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet();
244  }
245  }
246 
247  internal readonly struct TalentOption
248  {
249  private readonly ImmutableHashSet<Identifier> talentIdentifiers;
250 
251  public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
252 
256  public readonly int RequiredTalents;
260  public readonly int MaxChosenTalents;
261 
266  public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
267 
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;
271 
272  // No LINQ
273  public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
274  {
275  foreach (Identifier talent in selectedTalents)
276  {
277  if (talentIdentifiers.Contains(talent))
278  {
279  return true;
280  }
281  }
282  return false;
283  }
284 
285  public int CountMatchingTalents(IReadOnlyCollection<Identifier> talents)
286  {
287  int i = 0;
288  foreach (Identifier talent in talents)
289  {
290  if (talentIdentifiers.Contains(talent))
291  {
292  i++;
293  }
294  }
295  return i;
296  }
297 
298  public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
299  {
300  MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
301  RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
302 
303  if (RequiredTalents > MaxChosenTalents)
304  {
305  DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
306  contentPackage: talentOptionsElement.ContentPackage);
307  }
308 
309  HashSet<Identifier> identifiers = new HashSet<Identifier>();
310  foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
311  {
312  Identifier elementName = talentOptionElement.Name.ToIdentifier();
313  if (elementName == "talentoption")
314  {
315  identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty));
316  }
317  else if (elementName == "showcasetalent")
318  {
319  Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
320  HashSet<Identifier> showCaseTalentIdentifiers = new HashSet<Identifier>();
321  foreach (ContentXElement subElement in talentOptionElement.Elements())
322  {
323  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
324  showCaseTalentIdentifiers.Add(identifier);
325  identifiers.Add(identifier);
326  }
327  ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
328  }
329  }
330 
331  talentIdentifiers = identifiers.ToImmutableHashSet();
332 
333  if (RequiredTalents > talentIdentifiers.Count)
334  {
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);
337  }
338  if (MaxChosenTalents > talentIdentifiers.Count)
339  {
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);
342  }
343  }
344  }
345 }
readonly Identifier Identifier
Definition: Prefab.cs:34
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195
@ Character
Characters only