3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Globalization;
9 using Microsoft.Xna.Framework;
13 internal readonly
struct UpgradePrice
15 public readonly
int BasePrice;
17 public readonly
int IncreaseLow;
19 public readonly
int IncreaseHigh;
21 public UpgradePrice(UpgradePrefab prefab, ContentXElement element)
23 IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString(
"increaselow",
string.Empty)!,
24 "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings);
26 IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString(
"increasehigh",
string.Empty)!,
27 "IncreaseHigh".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings);
29 BasePrice = element.GetAttributeInt(
"baseprice", -1);
33 if (!prefab.SuppressWarnings)
35 DebugConsole.AddWarning($
"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " +
36 "The value has been assumed to be '1000'.",
37 prefab!.ContentPackage);
43 public int GetBuyPrice(UpgradePrefab prefab,
int level, Location? location =
null, ImmutableHashSet<Character>? characterList =
null)
45 float price = BasePrice;
47 int maxLevel = prefab.MaxLevel;
49 float lerpAmount = maxLevel is 0
51 : level / (float)maxLevel;
53 float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount);
54 price += price * (priceMultiplier / 100f);
56 price = location?.GetAdjustedMechanicalCost((
int)price) ?? price;
59 if (GameMain.GameSession?.Campaign is CampaignMode campaign)
61 price *= campaign.Settings.ShipyardPriceMultiplier;
64 characterList ??= GameSession.GetSessionCrewCharacters(
CharacterType.Both);
66 if (characterList.Any())
68 if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is
FactionAffiliation.Positive)
70 price *= 1f - characterList.Max(
static c => c.GetStatValue(
StatTypes.ShipyardBuyMultiplierAffiliated));
72 price *= 1f - characterList.Max(
static c => c.GetStatValue(
StatTypes.ShipyardBuyMultiplier));
81 onAdd: (prefab, isOverride) =>
83 if (prefab is UpgradePrefab upgradePrefab)
85 UpgradePrefab.Prefabs.Add(upgradePrefab, isOverride);
87 else if (prefab is UpgradeCategory upgradeCategory)
89 UpgradeCategory.Categories.Add(upgradeCategory, isOverride);
94 if (prefab is UpgradePrefab upgradePrefab)
96 UpgradePrefab.Prefabs.Remove(upgradePrefab);
98 else if (prefab is UpgradeCategory upgradeCategory)
100 UpgradeCategory.Categories.Remove(upgradeCategory);
105 UpgradePrefab.Prefabs.SortAll();
106 UpgradeCategory.Categories.SortAll();
108 onAddOverrideFile: (file) =>
110 UpgradePrefab.Prefabs.AddOverrideFile(file);
111 UpgradeCategory.Categories.AddOverrideFile(file);
113 onRemoveOverrideFile: (file) =>
115 UpgradePrefab.Prefabs.RemoveOverrideFile(file);
116 UpgradeCategory.Categories.RemoveOverrideFile(file);
122 internal class UpgradeCategory : UpgradeContentPrefab
124 public static readonly PrefabCollection<UpgradeCategory> Categories =
new PrefabCollection<UpgradeCategory>();
126 private readonly ImmutableHashSet<Identifier> selfItemTags;
127 private readonly HashSet<Identifier> prefabsThatAllowUpgrades =
new HashSet<Identifier>();
128 public readonly
bool IsWallUpgrade;
129 public readonly LocalizedString Name;
131 private readonly
object mutex =
new object();
133 public readonly IEnumerable<Identifier> ItemTags;
135 public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file)
137 selfItemTags = element.GetAttributeIdentifierArray(
"items", Array.Empty<Identifier>())?.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
138 Name = element.GetAttributeString(
"name",
string.Empty)!;
139 IsWallUpgrade = element.GetAttributeBool(
"wallupgrade",
false);
141 ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades);
143 Identifier nameIdentifier = element.GetAttributeIdentifier(
"nameidentifier", Identifier.Empty);
144 if (!nameIdentifier.IsEmpty)
146 Name = TextManager.Get($
"{nameIdentifier}");
148 else if (Name.IsNullOrWhiteSpace())
150 Name = TextManager.Get($
"UpgradeCategory.{Identifier}");
154 public void DeterminePrefabsThatAllowUpgrades()
158 prefabsThatAllowUpgrades.Clear();
159 prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs
160 .Where(it => it.GetAllowedUpgrades().Contains(Identifier))
161 .Select(it => it.Identifier));
165 public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab)
167 if (upgradePrefab !=
null && item.Submarine is { Info: var info } && !upgradePrefab.IsApplicable(info)) {
return false; }
170 switch (IsWallUpgrade)
174 case false when isStructure:
178 if (upgradePrefab !=
null && upgradePrefab.IsDisallowed(item)) {
return false; }
182 return item.Prefab.GetAllowedUpgrades().Contains(Identifier) ||
183 ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag);
187 public static UpgradeCategory? Find(Identifier identifier)
189 return !identifier.IsEmpty ? Categories.Find(category => category.Identifier == identifier) :
null;
192 public override void Dispose() { }
195 internal readonly
struct UpgradeMaxLevelMod
197 private enum MaxLevelModType
204 private readonly Either<SubmarineClass, int> tierOrClass;
205 private readonly
int value;
206 private readonly MaxLevelModType type;
208 public int GetLevelAfter(
int level) =>
211 MaxLevelModType.Invalid => level,
212 MaxLevelModType.Increase => level + value,
213 MaxLevelModType.Set => value,
214 _ =>
throw new ArgumentOutOfRangeException()
219 if (type is MaxLevelModType.Invalid) {
return false; }
221 if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata)
223 int modifier = metadata.GetInt(
new Identifier(
"tiermodifieroverride"), 0);
225 subTier = Math.Max(modifier, subTier);
228 if (tierOrClass.TryGet(out
int tier))
230 return subTier == tier;
235 return subClass == targetClass;
241 public UpgradeMaxLevelMod(ContentXElement element)
246 int tier = element.GetAttributeInt(
"tier", 0);
249 tierOrClass = subClass;
256 string stringValue = element.GetAttributeString(
"level",
null) ??
string.Empty;
259 if (
string.IsNullOrWhiteSpace(stringValue)) { isValid =
false; }
261 char firstChar = stringValue[0];
263 if (!
int.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) { isValid =
false; }
266 if (firstChar.Equals(
'+') || firstChar.Equals(
'-'))
268 type = MaxLevelModType.Increase;
272 type = MaxLevelModType.Set;
275 if (!isValid) { type = MaxLevelModType.Invalid; }
279 internal readonly
struct UpgradeResourceCost
281 public readonly
int Amount;
282 private readonly ImmutableArray<Identifier> targetTags;
283 public readonly Range<int> TargetLevels;
285 public UpgradeResourceCost(ContentXElement element)
287 Amount = element.GetAttributeInt(
"amount", 0);
288 targetTags = element.GetAttributeIdentifierArray(
"item", Array.Empty<Identifier>())!.ToImmutableArray();
289 TargetLevels = element.GetAttributeRange(
"levels",
new Range<int>(0, 99));
292 public bool AppliesForLevel(
int currentLevel) => TargetLevels.Contains(currentLevel);
294 public bool AppliesForLevel(Range<int> newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start;
296 public bool MatchesItem(
Item item) => MatchesItem(item.Prefab);
298 public bool MatchesItem(ItemPrefab item)
300 foreach (Identifier tag
in targetTags)
302 if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) {
return true; }
309 internal readonly
struct ApplicableResourceCollection
311 public readonly ImmutableArray<ItemPrefab> MatchingItems;
312 public readonly UpgradeResourceCost Cost;
313 public readonly
int Count;
315 public ApplicableResourceCollection(IEnumerable<ItemPrefab> matchingItems,
int count, UpgradeResourceCost cost)
317 MatchingItems = matchingItems.ToImmutableArray();
322 public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost)
324 return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost);
328 internal sealed
partial class UpgradePrefab : UpgradeContentPrefab
330 public static readonly PrefabCollection<UpgradePrefab> Prefabs =
new PrefabCollection<UpgradePrefab>(
331 onAdd:
static (prefab, isOverride) =>
333 if (!prefab.SuppressWarnings && !isOverride)
335 foreach (UpgradePrefab matchingPrefab in Prefabs?.Where(p => p != prefab && p.TargetItems.Any(s => prefab.TargetItems.Contains(s))) ?? throw new NullReferenceException(
"Honestly I have no clue why this could be null..."))
337 if (matchingPrefab.isOverride) { continue; }
339 var upgradePrefab = matchingPrefab.targetProperties;
340 string key = string.Empty;
342 if (upgradePrefab.Keys.Any(s => prefab.targetProperties.Keys.Any(s1 => s == (key = s1))))
344 if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => prefab.targetProperties[key].Contains(s)))
346 DebugConsole.AddWarning($
"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" +
347 "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" +
348 "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing.",
349 prefab.ContentPackage);
357 onAddOverrideFile:
null,
358 onRemoveOverrideFile: null
364 public readonly
int MaxLevel;
366 public LocalizedString Name {
get; }
368 public LocalizedString Description {
get; }
370 public float IncreaseOnTooltip {
get; }
372 private readonly ImmutableHashSet<Identifier> upgradeCategoryIdentifiers;
374 public IEnumerable<UpgradeCategory> UpgradeCategories
378 foreach (var
id in upgradeCategoryIdentifiers)
380 if (UpgradeCategory.Categories.TryGet(
id, out var category)) { yield
return category!; }
385 public UpgradePrice Price {
get; }
387 private bool isOverride => Prefabs.IsOverride(
this);
389 public ContentXElement SourceElement {
get; }
391 public bool SuppressWarnings {
get; }
395 public IEnumerable<Identifier> TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags);
397 public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade);
399 private Dictionary<string, string[]> targetProperties {
get; }
400 private readonly ImmutableArray<UpgradeMaxLevelMod> MaxLevelsMods;
401 public readonly ImmutableHashSet<UpgradeResourceCost> ResourceCosts;
403 public const int CrushDepthDefaultUpgradePrc = 15;
404 private static int? crushDepthUpgradePrc;
406 public static int CrushDepthUpgradePrc
410 if (crushDepthUpgradePrc ==
null)
412 if (Find(
"increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab)
414 string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement(
"Structure")?.GetAttributeString(
"crushdepth",
null) ??
string.Empty;
415 if (!
string.IsNullOrEmpty(updateValueStr))
417 crushDepthUpgradePrc = ParsePercentage(updateValueStr, Identifier.Empty, suppressWarnings:
true);
422 return crushDepthUpgradePrc ?? CrushDepthDefaultUpgradePrc;
426 public const int IncreaseWallHealthDefaultMaxLevel = 6;
427 private static int? increaseWallHealthMaxLevel;
429 public static int IncreaseWallHealthMaxLevel
433 if (increaseWallHealthMaxLevel ==
null)
435 if (Find(
"increasewallhealth".ToIdentifier()) is UpgradePrefab hullUpgradePrefab)
437 increaseWallHealthMaxLevel = hullUpgradePrefab.MaxLevel;
441 return increaseWallHealthMaxLevel ?? IncreaseWallHealthDefaultMaxLevel;
445 public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
447 Name = element.GetAttributeString(nameof(Name),
string.Empty)!;
448 Description = element.GetAttributeString(nameof(Description),
string.Empty)!;
449 MaxLevel = element.GetAttributeInt(nameof(MaxLevel), 1);
450 SuppressWarnings = element.GetAttributeBool(nameof(SuppressWarnings),
false);
451 HideInMenus = element.GetAttributeBool(nameof(HideInMenus),
false);
452 SourceElement = element;
454 var targetProperties =
new Dictionary<string, string[]>();
455 var maxLevels =
new List<UpgradeMaxLevelMod>();
456 var resourceCosts =
new HashSet<UpgradeResourceCost>();
458 Identifier nameIdentifier = element.GetAttributeIdentifier(
"nameidentifier",
"");
459 if (!nameIdentifier.IsEmpty)
461 Name = TextManager.Get($
"UpgradeName.{nameIdentifier}");
463 else if (Name.IsNullOrWhiteSpace())
465 Name = TextManager.Get($
"UpgradeName.{Identifier}");
468 Identifier descriptionIdentifier = element.GetAttributeIdentifier(
"descriptionidentifier",
"");
469 if (!descriptionIdentifier.IsEmpty)
471 Description = TextManager.Get($
"UpgradeDescription.{descriptionIdentifier}");
473 else if (Description.IsNullOrWhiteSpace())
475 Description = TextManager.Get($
"UpgradeDescription.{Identifier}");
478 IncreaseOnTooltip = element.GetAttributeFloat(
"increaseontooltip", 0f);
480 DebugConsole.Log(
" " + Name);
483 var decorativeSprites =
new List<DecorativeSprite>();
485 foreach (var subElement
in element.Elements())
487 switch (subElement.Name.ToString().ToLowerInvariant())
491 Price =
new UpgradePrice(
this, subElement);
496 maxLevels.Add(
new UpgradeMaxLevelMod(subElement));
501 resourceCosts.Add(
new UpgradeResourceCost(subElement));
505 case "decorativesprite":
507 decorativeSprites.Add(
new DecorativeSprite(subElement));
512 Sprite =
new Sprite(subElement);
516 case "decorativesprite":
522 IEnumerable<string> properties = subElement.Attributes().Select(attribute => attribute.Name.ToString());
523 targetProperties.Add(subElement.Name.ToString(), properties.ToArray());
530 DecorativeSprites = decorativeSprites.ToImmutableArray();
533 this.targetProperties = targetProperties;
534 MaxLevelsMods = maxLevels.ToImmutableArray();
535 ResourceCosts = resourceCosts.ToImmutableHashSet();
537 upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray(
"categories", Array.Empty<Identifier>())?
538 .ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
544 public int GetMaxLevelForCurrentSub()
547 return sub is { Info: var info } ? GetMaxLevel(info) : MaxLevel;
553 public int GetMaxLevel(SubmarineInfo info)
555 int level = MaxLevel;
557 int tier = info.Tier;
559 if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata)
561 int modifier = metadata.GetInt(
new Identifier($
"tiermodifiers.{Identifier}"), 0);
565 tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier);
567 foreach (UpgradeMaxLevelMod mod
in MaxLevelsMods)
569 if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); }
575 public bool IsApplicable(SubmarineInfo? info)
577 if (info is
null) {
return false; }
579 return GetMaxLevel(info) > 0;
582 public bool HasResourcesToUpgrade(Character? character,
int currentLevel)
584 if (character is
null) {
return false; }
585 if (!ResourceCosts.Any()) {
return true; }
587 var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
589 return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
593 public bool TryTakeResources(Character character,
int currentLevel)
595 var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
597 if (!costs.Any()) {
return true; }
599 var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character);
600 HashSet<Item> itemsToRemove =
new HashSet<Item>();
602 foreach (UpgradeResourceCost cost
in costs)
604 int amountNeeded = cost.Amount;
605 foreach (
Item item
in inventoryItems.Where(cost.MatchesItem))
607 itemsToRemove.Add(item);
609 if (amountNeeded <= 0) {
break; }
612 if (amountNeeded > 0) {
return false; }
615 foreach (
Item item
in itemsToRemove)
617 Entity.Spawner.AddItemToRemoveQueue(item);
620 if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
625 public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(
int level)
627 var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
629 var costs = applicableCosts.Any()
630 ? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
631 : ImmutableArray<ApplicableResourceCollection>.Empty;
636 public bool IsDisallowed(MapEntity item)
638 return item.DisallowedUpgradeSet.Contains(Identifier)
639 || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier));
642 public static UpgradePrefab? Find(Identifier identifier)
644 return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) :
null;
662 public static int ParsePercentage(
string value, Identifier attribute =
default, XElement? sourceElement =
null,
bool suppressWarnings =
false)
664 string? line = sourceElement?.ToString().Split(
'\n')[0].Trim();
665 bool doWarnings = !suppressWarnings && !attribute.IsEmpty && sourceElement !=
null && line !=
null;
667 if (
string.IsNullOrWhiteSpace(value))
671 DebugConsole.AddWarning($
"Attribute \"{attribute}\" not found at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
672 "Value has been assumed to be '0'.");
678 if (!
int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var price))
682 if (str.Length > 1 && str[0] ==
'+') { str = str.Substring(1); }
684 if (str.Length > 1 && str[^1] ==
'%') { str = str.Substring(0, str.Length - 1); }
686 if (
int.TryParse(str, out price))
698 DebugConsole.AddWarning($
"Value in attribute \"{attribute}\" is not formatted correctly\n " +
699 $
"at {sourceElement!.Document?.ParseContentPathFromUri()} @ '{line}'.\n " +
700 "It should be an integer with optionally a '+' or '-' at the front and/or '%' at the end.\n" +
701 "The value has been assumed to be '0'.");
707 public override void Dispose()
712 DecorativeSprites.ForEach(sprite => sprite.Remove());
713 targetProperties.Clear();
UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file)
static readonly PrefabCollection< UpgradeContentPrefab > PrefabsAndCategories
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
@ Structure
Structures and hulls, but also items (for backwards support)!