Client LuaCsForBarotrauma
Location.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
8 
9 namespace Barotrauma
10 {
11  partial class Location
12  {
13  public class TakenItem
14  {
15  public readonly ushort OriginalID;
16  public readonly ushort ModuleIndex;
17  public readonly Identifier Identifier;
18  public readonly int OriginalContainerIndex;
19 
20  public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
21  {
22  OriginalID = originalID;
23  OriginalContainerIndex = originalContainerIndex;
24  ModuleIndex = moduleIndex;
25  Identifier = identifier;
26  }
27 
28  public TakenItem(Item item)
29  {
30  System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items");
31 
33  OriginalID = item.ID;
34  ModuleIndex = (ushort) item.OriginalModuleIndex;
36  }
37 
38  public bool IsEqual(TakenItem obj)
39  {
41  }
42 
43  public bool Matches(Item item)
44  {
46  {
47  return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
48  }
49  else
50  {
51  return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier;
52  }
53  }
54  }
55 
56  public readonly List<LocationConnection> Connections = new List<LocationConnection>();
57 
58  public LocalizedString DisplayName { get; private set; }
59 
60  public Identifier NameIdentifier => nameIdentifier;
61 
62  private int nameFormatIndex;
63  private Identifier nameIdentifier;
64 
68  private string rawName;
69 
70  private LocationType addInitialMissionsForType;
71 
72  public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
73 
74  public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false;
75 
76  public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
77  public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
79 
83  public bool LocationTypeChangesBlocked => DisallowLocationTypeChanges || availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges);
84 
86 
87  public Biome Biome { get; set; }
88 
89  public Vector2 MapPosition { get; private set; }
90 
91  public LocationType Type { get; private set; }
92 
93  public LocationType OriginalType { get; private set; }
94 
95  public LevelData LevelData { get; set; }
96 
97  public int PortraitId { get; private set; }
98 
99  public Faction Faction { get; set; }
100 
101  public Faction SecondaryFaction { get; set; }
102 
104 
106 
107  public int TurnsInRadiation { get; set; }
108 
109  #region Store
110 
111  public class StoreInfo
112  {
113  public Identifier Identifier { get; }
114  public Identifier MerchantFaction { get; private set; }
115  public int Balance { get; set; }
116  public List<PurchasedItem> Stock { get; } = new List<PurchasedItem>();
117  public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
118  public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
122  public int PriceModifier { get; set; }
123  public Location Location { get; }
124  private float MaxReputationModifier => Location.StoreMaxReputationModifier;
125 
126  private StoreInfo(Location location)
127  {
128  Location = location;
129  }
130 
134  public StoreInfo(Location location, Identifier identifier) : this(location)
135  {
136  Identifier = identifier;
137  Balance = location.StoreInitialBalance;
138  Stock = CreateStock();
141  }
142 
146  public StoreInfo(Location location, XElement storeElement) : this(location)
147  {
148  Identifier = storeElement.GetAttributeIdentifier("identifier", "");
149  MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), "");
150  Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance);
151  PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
152  // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute
153  if (storeElement.Attribute("stepssincespecialsupdated") != null)
154  {
155  location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
156  }
157  foreach (var stockElement in storeElement.GetChildElements("stock"))
158  {
159  var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty);
160  if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; }
161  int qty = stockElement.GetAttributeInt("qty", 0);
162  if (qty < 1) { continue; }
163  Stock.Add(new PurchasedItem(prefab, qty, buyer: null));
164  }
165  if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
166  {
167  var loadedDailySpecials = LoadStoreSpecials(specialsElement);
168  DailySpecials.AddRange(loadedDailySpecials);
169  }
170  if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
171  {
172  var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
173  RequestedGoods.AddRange(loadedRequestedGoods);
174  }
175 
176  static List<ItemPrefab> LoadStoreSpecials(XElement element)
177  {
178  var specials = new List<ItemPrefab>();
179  foreach (var childElement in element.GetChildElements("item"))
180  {
181  var id = childElement.GetAttributeIdentifier("id", Identifier.Empty);
182  if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; }
183  specials.Add(prefab);
184  }
185  return specials;
186  }
187  }
188 
189  public static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
190  {
191  int quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1);
192  return new PurchasedItem(itemPrefab, quantity, buyer: null);
193  }
194 
195  public List<PurchasedItem> CreateStock()
196  {
197  var stock = new List<PurchasedItem>();
198  foreach (var prefab in ItemPrefab.Prefabs)
199  {
200  if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; }
201  stock.Add(CreateInitialStockItem(prefab, priceInfo));
202  }
203  return stock;
204  }
205 
206  public void AddStock(List<SoldItem> items)
207  {
208  if (items == null || items.None()) { return; }
209  DebugConsole.NewMessage($"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
210  foreach (var item in items)
211  {
212  if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
213  {
214  stockItem.Quantity += 1;
215  DebugConsole.NewMessage($"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
216  }
217  else
218  {
219  DebugConsole.NewMessage($"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly: true);
220  }
221  }
222  }
223 
224  public void RemoveStock(List<PurchasedItem> items)
225  {
226  if (items == null || items.None()) { return; }
227  DebugConsole.NewMessage($"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true);
228  foreach (PurchasedItem item in items)
229  {
230  if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem)
231  {
232  stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0);
233  DebugConsole.NewMessage($"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true);
234  }
235  }
236  }
237 
238  public void GenerateSpecials()
239  {
240  var availableStock = new Dictionary<ItemPrefab, float>();
241  foreach (var stockItem in Stock)
242  {
243  if (stockItem.Quantity < 1) { continue; }
244  float weight = 1.0f;
245  if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo)
246  {
247  if (!priceInfo.CanBeSpecial) { continue; }
248  var baseQuantity = priceInfo.MinAvailableAmount;
249  weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
250  if (weight < 0.0f) { continue; }
251  }
252  availableStock.Add(stockItem.ItemPrefab, weight);
253  }
254  DailySpecials.Clear();
255  int extraSpecialSalesCount = GetExtraSpecialSalesCount();
256  for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++)
257  {
258  if (availableStock.None()) { break; }
259  var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
260  if (item == null) { break; }
261  DailySpecials.Add(item);
262  availableStock.Remove(item);
263  }
264  RequestedGoods.Clear();
265  for (int i = 0; i < Location.RequestedGoodsCount; i++)
266  {
267  var item = ItemPrefab.Prefabs.GetRandom(p =>
268  p.CanBeSold && !RequestedGoods.Contains(p) &&
269  p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced);
270  if (item == null) { break; }
271  RequestedGoods.Add(item);
272  }
273  Location.StepsSinceSpecialsUpdated = 0;
274  }
275 
276  public void GeneratePriceModifier()
277  {
278  PriceModifier = Rand.Range(-Location.StorePriceModifierRange, Location.StorePriceModifierRange + 1);
279  }
280 
283  public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
284  {
285  priceInfo ??= item?.GetPriceInfo(this);
286  if (priceInfo == null) { return 0; }
287  float price = priceInfo.Price;
288  // Adjust by random price modifier
289  price = (100 + PriceModifier) / 100.0f * price;
290  price *= priceInfo.BuyingPriceMultiplier;
291  // Adjust by daily special status
292  if (considerDailySpecials && DailySpecials.Contains(item))
293  {
294  price = Location.DailySpecialPriceModifier * price;
295  }
296  // Adjust by current reputation
297  price *= GetReputationModifier(true);
298 
299  // Adjust by campaign difficulty settings
300  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
301  {
302  price *= campaign.Settings.ShopPriceMultiplier;
303  }
304 
305  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
306  if (characters.Any())
307  {
309  if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
310  {
311  price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
312  price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
313  price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
314  }
315  price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
316  price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
317  }
318  // Price should never go below 1 mk
319  return Math.Max((int)price, 1);
320  }
321 
324  public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
325  {
326  priceInfo ??= item?.GetPriceInfo(this);
327  if (priceInfo == null) { return 0; }
328  float price = Location.StoreSellPriceModifier * priceInfo.Price;
329  // Adjust by random price modifier
330  price = (100 - PriceModifier) / 100.0f * price;
331  // Adjust by requested good status
332  if (considerRequestedGoods && RequestedGoods.Contains(item))
333  {
334  price = Location.RequestGoodPriceModifier * price;
335  }
336  // Adjust by location reputation
337  price *= GetReputationModifier(false);
338 
339  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
340  if (characters.Any())
341  {
342  price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
343  price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
344  }
345 
346  // Price should never go below 1 mk
347  return Math.Max((int)price, 1);
348  }
349 
350  public void SetMerchantFaction(Identifier factionIdentifier)
351  {
352  MerchantFaction = factionIdentifier;
353  }
354 
356  {
357  return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty);
358  }
359 
360  public float GetReputationModifier(bool buying)
361  {
362  var factionIdentifier = GetMerchantOrLocationFactionIdentifier();
363  var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation;
364  if (reputation == null) { return 1.0f; }
365  if (buying)
366  {
367  if (reputation.Value > 0.0f)
368  {
369  return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
370  }
371  else
372  {
373  return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
374  }
375  }
376  else
377  {
378  if (reputation.Value > 0.0f)
379  {
380  return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
381  }
382  else
383  {
384  return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
385  }
386  }
387  }
388 
389  public override string ToString()
390  {
391  return Identifier.Value;
392  }
393  }
394 
395  public Dictionary<Identifier, StoreInfo> Stores { get; set; }
396 
397  private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier;
398  private float StoreSellPriceModifier => Type.StoreSellPriceModifier;
399  private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier;
400  private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier;
402  private int StorePriceModifierRange => Type.StorePriceModifierRange;
403 
407  private const int SpecialsUpdateInterval = 3;
410  private int StepsSinceSpecialsUpdated { get; set; }
411  public HashSet<Identifier> StoreIdentifiers { get; } = new HashSet<Identifier>();
412 
413  #endregion
414 
415  private const float MechanicalMaxDiscountPercentage = 50.0f;
416  private const float HealMaxDiscountPercentage = 10.0f;
417 
418  private readonly List<TakenItem> takenItems = new List<TakenItem>();
419  public IEnumerable<TakenItem> TakenItems
420  {
421  get { return takenItems; }
422  }
423 
424  private readonly HashSet<int> killedCharacterIdentifiers = new HashSet<int>();
425  public IEnumerable<int> KilledCharacterIdentifiers
426  {
427  get { return killedCharacterIdentifiers; }
428  }
429 
430  private readonly List<Mission> availableMissions = new List<Mission>();
431  public IEnumerable<Mission> AvailableMissions
432  {
433  get
434  {
435  availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
436  return availableMissions;
437  }
438  }
439 
440  private readonly List<Mission> selectedMissions = new List<Mission>();
441  public IEnumerable<Mission> SelectedMissions
442  {
443  get
444  {
445  selectedMissions.RemoveAll(m => !availableMissions.Contains(m));
446  return selectedMissions;
447  }
448  }
449 
450 
451 
452  public void SelectMission(Mission mission)
453  {
454  if (!SelectedMissions.Contains(mission) && mission != null)
455  {
456  selectedMissions.Add(mission);
457  selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
458  }
459  }
460 
461  public void DeselectMission(Mission mission)
462  {
463  selectedMissions.Remove(mission);
464  }
465 
466 
467  public List<int> GetSelectedMissionIndices()
468  {
469  List<int> selectedMissionIndices = new List<int>();
470  foreach (Mission mission in SelectedMissions)
471  {
472  if (availableMissions.Contains(mission))
473  {
474  selectedMissionIndices.Add(availableMissions.IndexOf(mission));
475  }
476  }
477  return selectedMissionIndices;
478  }
479 
480  public void SetSelectedMissionIndices(IEnumerable<int> missionIndices)
481  {
482  selectedMissions.Clear();
483  foreach (int missionIndex in missionIndices)
484  {
485  if (missionIndex < 0 || missionIndex >= availableMissions.Count)
486  {
487  DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
488  break;
489  }
490  selectedMissions.Add(availableMissions[missionIndex]);
491  }
492  }
493 
494  private float priceMultiplier = 1.0f;
495  public float PriceMultiplier
496  {
497  get { return priceMultiplier; }
498  set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
499  }
500 
501  private float mechanicalpriceMultiplier = 1.0f;
503  {
504  get => mechanicalpriceMultiplier;
505  set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
506  }
507 
508  public string LastTypeChangeMessage;
509 
511 
512  public bool IsGateBetweenBiomes;
513 
514  private readonly struct LoadedMission
515  {
516  public readonly MissionPrefab MissionPrefab;
517  public readonly int TimesAttempted;
518  public readonly int OriginLocationIndex;
519  public readonly int DestinationIndex;
520  public readonly bool SelectedMission;
521 
522  public LoadedMission(XElement element)
523  {
524  var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty);
525  MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null;
526  TimesAttempted = element.GetAttributeInt("timesattempted", 0);
527  OriginLocationIndex = element.GetAttributeInt("origin", -1);
528  DestinationIndex = element.GetAttributeInt("destinationindex", -1);
529  SelectedMission = element.GetAttributeBool("selected", false);
530  }
531  }
532 
533  private List<LoadedMission> loadedMissions;
534 
536 
537  public override string ToString()
538  {
539  return $"Location ({DisplayName ?? "null"})";
540  }
541 
542  public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
543  {
544  Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
545  CreateRandomName(Type, rand, existingLocations);
546  MapPosition = mapPosition;
547  PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
548  Connections = new List<LocationConnection>();
549  }
550 
554  public Location(CampaignMode campaign, XElement element)
555  {
556  Identifier locationTypeId = element.GetAttributeIdentifier("type", "");
557  bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type);
558  Type = type;
559 
560  Identifier originalLocationTypeId = element.GetAttributeIdentifier("originaltype", locationTypeId);
561  GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
562  OriginalType = originalType;
563 
564  nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
565  if (nameIdentifier.IsEmpty)
566  {
567  //backwards compatibility
568  DisplayName = element.GetAttributeString("name", "");
569  rawName = element.GetAttributeString("rawname", element.GetAttributeString("basename", DisplayName.Value));
570  nameIdentifier = rawName.ToIdentifier();
571  }
572  else
573  {
574  nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
575  DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
576  }
577 
578  MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
579 
580  PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
581  IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
582  MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
583  TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
584  StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0);
585 
586  var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty);
587  if (!factionIdentifier.IsEmpty)
588  {
589  Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
590  }
591  var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty);
592  if (!secondaryFactionIdentifier.IsEmpty)
593  {
594  SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
595  }
596  Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty);
597  if (biomeId != Identifier.Empty)
598  {
599  if (Biome.Prefabs.TryGet(biomeId, out Biome biome))
600  {
601  Biome = biome;
602  }
603  else
604  {
605  DebugConsole.ThrowError($"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
606  }
607  }
608 
609  if (!typeNotFound)
610  {
611  for (int i = 0; i < Type.CanChangeTo.Count; i++)
612  {
613  for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
614  {
615  ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0));
616  }
617  }
618 
619  LoadLocationTypeChange(element);
620  }
621 
622  string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty<string>());
623  foreach (string takenItem in takenItemStr)
624  {
625  string[] takenItemSplit = takenItem.Split(';');
626  if (takenItemSplit.Length != 4)
627  {
628  DebugConsole.ThrowError($"Error in saved location: could not parse taken item data \"{takenItem}\"");
629  continue;
630  }
631  if (!ushort.TryParse(takenItemSplit[1], out ushort id))
632  {
633  DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
634  continue;
635  }
636  if (!int.TryParse(takenItemSplit[2], out int containerIndex))
637  {
638  DebugConsole.ThrowError($"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\"");
639  continue;
640  }
641  if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
642  {
643  DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
644  continue;
645  }
646  takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex));
647  }
648 
649  killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
650 
651  System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
652  Type ??= LocationType.Prefabs.First();
653 
654  LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
655 
656  PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
657 
658  LoadStores(element);
659  LoadMissions(element);
660 
661  bool GetTypeOrFallback(Identifier identifier, out LocationType type)
662  {
663  if (!LocationType.Prefabs.TryGet(identifier, out type))
664  {
665  //turn lairs into abandoned outposts
666  if (identifier == "lair")
667  {
668  LocationType.Prefabs.TryGet("Abandoned".ToIdentifier(), out type);
669  addInitialMissionsForType = Type;
670  }
671  if (type == null)
672  {
673  DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead.");
674  LocationType.Prefabs.TryGet("None".ToIdentifier(), out type);
675  type ??= LocationType.Prefabs.First();
676  }
677  if (type != null)
678  {
679  element.SetAttributeValue("type", type.Identifier.ToString());
680  }
681  return false;
682  }
683  return true;
684  }
685  }
686 
687  public void LoadLocationTypeChange(XElement locationElement)
688  {
689  TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
690  LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
691  foreach (var subElement in locationElement.Elements())
692  {
693  switch (subElement.Name.ToString())
694  {
695  case "pendinglocationtypechange":
696  int timer = subElement.GetAttributeInt("timer", 0);
697  if (subElement.Attribute("index") != null)
698  {
699  int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
700  if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
701  {
702  DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
703  continue;
704  }
705  PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
706  }
707  else
708  {
709  Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", "");
710  var mission = MissionPrefab.Prefabs[missionIdentifier];
711  if (mission == null)
712  {
713  DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
714  continue;
715  }
716  PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
717  }
718  break;
719  }
720  }
721  }
722 
723  public void LoadMissions(XElement locationElement)
724  {
725  if (locationElement.GetChildElement("missions") is XElement missionsElement)
726  {
727  loadedMissions = new List<LoadedMission>();
728  foreach (XElement childElement in missionsElement.GetChildElements("mission"))
729  {
730  var loadedMission = new LoadedMission(childElement);
731  if (loadedMission.MissionPrefab != null)
732  {
733  loadedMissions.Add(loadedMission);
734  }
735  }
736  }
737  }
738 
739  public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
740  {
741  return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
742  }
743 
744  public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
745  {
746  if (newType == Type) { return; }
747 
748  if (newType == null)
749  {
750  DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
751  return;
752  }
753 
754  Type = newType;
755  if (rawName != null)
756  {
757  DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
758  DisplayName =
759  Type.NameFormats == null || !Type.NameFormats.Any() ?
760  rawName :
761  Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
762  }
763  else
764  {
765  DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
766  DisplayName =
767  Type.NameFormats == null || !Type.NameFormats.Any() ?
768  TextManager.Get(nameIdentifier) :
769  Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
770  }
771 
773  if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
774  {
775  if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); }
776  if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); }
777  }
778  else
779  {
780  if (Type.Faction == Identifier.Empty) { Faction = null; }
781  if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
782  }
783 
784  UnlockInitialMissions(Rand.RandSync.Unsynced);
785 
786  if (createStores)
787  {
788  CreateStores(force: true);
789  }
790  }
791 
793  {
794  if (campaign == null) { return; }
795  if (Type.Faction != Identifier.Empty)
796  {
797  Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction);
798  }
799  if (Type.SecondaryFaction != Identifier.Empty)
800  {
801  SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction);
802  }
803 
804  Faction TryFindFaction(Identifier identifier)
805  {
806  var faction = campaign.GetFaction(identifier);
807  if (faction == null)
808  {
809  DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
810  contentPackage: Type.ContentPackage);
811  }
812  return faction;
813  }
814  }
815 
816  public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
817  {
818  if (Type.MissionIdentifiers.Any())
819  {
820  UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
821  }
822  if (Type.MissionTags.Any())
823  {
824  UnlockMissionByTag(Type.MissionTags.GetRandom(randSync), invokingContentPackage: Type.ContentPackage);
825  }
826  }
827 
828  public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
829  {
830  if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
831  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
832  AddMission(InstantiateMission(missionPrefab, connection));
833  }
834 
835  public void UnlockMission(MissionPrefab missionPrefab)
836  {
837  if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
838  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; }
839  AddMission(InstantiateMission(missionPrefab));
840  }
841 
842  public Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage = null)
843  {
844  if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; }
845  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
846 
847  var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier);
848  if (missionPrefab == null)
849  {
850  DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.",
851  contentPackage: invokingContentPackage);
852  }
853  else
854  {
855  var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
856  //don't allow duplicate missions in the same connection
857  if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
858  {
859  return null;
860  }
861  AddMission(mission);
862  DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true);
863  return mission;
864  }
865  return null;
866  }
867 
868  public Mission UnlockMissionByTag(Identifier tag, Random random = null, ContentPackage invokingContentPackage = null)
869  {
870  if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; }
871  var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag));
872  if (matchingMissions.None())
873  {
874  DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage);
875  }
876  else
877  {
878  var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m));
879  if (unusedMissions.Any())
880  {
881  var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
882  if (suitableMissions.None())
883  {
884  suitableMissions = unusedMissions;
885  }
886  var filteredMissions = suitableMissions.Where(m => LevelData.Difficulty >= m.MinLevelDifficulty && LevelData.Difficulty <= m.MaxLevelDifficulty);
887  if (filteredMissions.None())
888  {
889  DebugConsole.AddWarning($"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.",
890  contentPackage: invokingContentPackage);
891  }
892  else
893  {
894  suitableMissions = filteredMissions;
895  }
896  MissionPrefab missionPrefab =
897  random != null ?
898  ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) :
899  ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced);
900 
901  var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
902  //don't allow duplicate missions in the same connection
903  if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
904  {
905  return null;
906  }
907  AddMission(mission);
908  DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly: true);
909  return mission;
910  }
911  else
912  {
913  DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.",
914  contentPackage: invokingContentPackage);
915  }
916  }
917 
918  return null;
919  }
920 
921  private void AddMission(Mission mission)
922  {
923  if (!mission.Prefab.AllowOtherMissionsInLevel)
924  {
925  availableMissions.Clear();
926  }
927  availableMissions.Add(mission);
928 #if CLIENT
929  GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
930 #else
931  (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
932 #endif
933  }
934 
935  private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
936  {
937  if (prefab.IsAllowed(this, this))
938  {
939  connection = null;
940  return InstantiateMission(prefab);
941  }
942 
943  var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
944  if (suitableConnections.None())
945  {
946  suitableConnections = Connections.ToList();
947  }
948  //prefer connections that haven't been passed through, and connections with fewer available missions
949  connection = ToolBox.SelectWeightedRandom(
950  suitableConnections.ToList(),
951  suitableConnections.Select(c => GetConnectionWeight(this, c)).ToList(),
952  Rand.RandSync.Unsynced);
953 
954  static float GetConnectionWeight(Location location, LocationConnection c)
955  {
956  Location destination = c.OtherLocation(location);
957  if (destination == null) { return 0; }
958  float minWeight = 0.0001f;
959  float lowWeight = 0.2f;
960  float normalWeight = 1.0f;
961  float maxWeight = 2.0f;
962  float weight = c.Passed ? lowWeight : normalWeight;
963  if (location.Biome.AllowedZones.Contains(1))
964  {
965  // In the first biome, give a stronger preference for locations that are farther to the right)
966  float diff = destination.MapPosition.X - location.MapPosition.X;
967  if (diff < 0)
968  {
969  weight *= 0.1f;
970  }
971  else
972  {
973  float maxRelevantDiff = 300;
974  weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
975  }
976  }
977  else if (destination.MapPosition.X > location.MapPosition.X)
978  {
979  weight *= 2.0f;
980  }
981  int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
982  if (missionCount > 0)
983  {
984  weight /= missionCount * 2;
985  }
986  if (destination.IsRadiated())
987  {
988  weight *= 0.001f;
989  }
990  return MathHelper.Clamp(weight, minWeight, maxWeight);
991  }
992 
993  return InstantiateMission(prefab, connection);
994  }
995 
996  private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
997  {
998  Location destination = connection.OtherLocation(this);
999  var mission = prefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
1000  mission.AdjustLevelData(connection.LevelData);
1001  return mission;
1002  }
1003 
1004  private Mission InstantiateMission(MissionPrefab prefab)
1005  {
1006  var mission = prefab.Instantiate(new Location[] { this, this }, Submarine.MainSub);
1007  mission.AdjustLevelData(LevelData);
1008  return mission;
1009  }
1010 
1012  {
1013  availableMissions.Clear();
1014  selectedMissions.Clear();
1015  if (loadedMissions != null && loadedMissions.Any())
1016  {
1017  foreach (LoadedMission loadedMission in loadedMissions)
1018  {
1019  Location destination;
1020  if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
1021  {
1022  destination = map.Locations[loadedMission.DestinationIndex];
1023  }
1024  else
1025  {
1026  destination = Connections.First().OtherLocation(this);
1027  }
1028  var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
1029  if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
1030  {
1031  mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
1032  }
1033  mission.TimesAttempted = loadedMission.TimesAttempted;
1034  availableMissions.Add(mission);
1035  if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
1036  }
1037  loadedMissions = null;
1038  }
1039  if (addInitialMissionsForType != null)
1040  {
1041  if (addInitialMissionsForType.MissionIdentifiers.Any())
1042  {
1043  UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
1044  }
1045  if (addInitialMissionsForType.MissionTags.Any())
1046  {
1047  UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage);
1048  }
1049  addInitialMissionsForType = null;
1050  }
1051  }
1052 
1056  public void ClearMissions()
1057  {
1058  availableMissions.Clear();
1059  selectedMissions.Clear();
1060  }
1061 
1062  public bool HasOutpost()
1063  {
1064  if (!Type.HasOutpost) { return false; }
1065 
1066  return !IsCriticallyRadiated();
1067  }
1068 
1069  public bool IsCriticallyRadiated()
1070  {
1071  if (GameMain.GameSession?.Map?.Radiation != null)
1072  {
1073  return TurnsInRadiation > GameMain.GameSession.Map.Radiation.Params.CriticalRadiationThreshold;
1074  }
1075 
1076  return false;
1077  }
1078 
1080  {
1081  if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty)
1082  {
1083  if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType))
1084  {
1085  return newLocationType;
1086  }
1087  else
1088  {
1089  DebugConsole.ThrowError($"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found.");
1090  }
1091  }
1092  return Type;
1093  }
1094 
1095  public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
1096  {
1097  System.Diagnostics.Debug.Assert(Connections.Contains(connection));
1098  return AvailableMissions.Where(m => m.Locations[1] == connection.OtherLocation(this));
1099  }
1100 
1102  {
1104  {
1105  DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
1106  return;
1107  }
1108  if (HireManager == null)
1109  {
1110  DebugConsole.ThrowErrorLocalized("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
1111  return;
1112  }
1113 
1114  HireManager.RemoveCharacter(character);
1115  }
1116 
1117  public IEnumerable<CharacterInfo> GetHireableCharacters()
1118  {
1120  {
1121  return Enumerable.Empty<CharacterInfo>();
1122  }
1123 
1124  HireManager ??= new HireManager();
1125 
1126  if (!HireManager.AvailableCharacters.Any())
1127  {
1129  }
1131  }
1132 
1133  public void ForceHireableCharacters(IEnumerable<CharacterInfo> hireableCharacters)
1134  {
1135  HireManager ??= new HireManager();
1136  HireManager.AvailableCharacters = hireableCharacters.ToList();
1137  }
1138 
1139  private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
1140  {
1141  if (!type.ForceLocationName.IsEmpty)
1142  {
1143  nameIdentifier = type.ForceLocationName;
1144  DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
1145  return;
1146  }
1147  nameIdentifier = type.GetRandomNameId(rand, existingLocations);
1148  if (nameIdentifier.IsEmpty)
1149  {
1150  //backwards compatibility
1151  rawName = type.GetRandomRawName(rand, existingLocations);
1152  if (rawName.IsNullOrEmpty())
1153  {
1154  DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files.");
1155  rawName = "none";
1156  }
1157  nameIdentifier = rawName.ToIdentifier();
1158  if (type.NameFormats == null || !type.NameFormats.Any())
1159  {
1160  DisplayName = rawName;
1161  }
1162  else
1163  {
1164  nameFormatIndex = rand.Next() % type.NameFormats.Count;
1165  DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName);
1166  }
1167  }
1168  else
1169  {
1170  if (type.NameFormats == null || !type.NameFormats.Any())
1171  {
1172  DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
1173  return;
1174  }
1175  nameFormatIndex = rand.Next() % type.NameFormats.Count;
1176  DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
1177  }
1178  }
1179 
1180  private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
1181  {
1182  if (type?.NameFormats == null || !type.NameFormats.Any())
1183  {
1184  return TextManager.Get(nameId);
1185  }
1186  return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
1187  }
1188 
1189  public void ForceName(Identifier nameId)
1190  {
1191  rawName = string.Empty;
1192  nameIdentifier = nameId;
1193  DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
1194  }
1195 
1196  public void LoadStores(XElement locationElement)
1197  {
1198  UpdateStoreIdentifiers();
1199  Stores?.Clear();
1200  foreach (var storeElement in locationElement.GetChildElements("store"))
1201  {
1202  Stores ??= new Dictionary<Identifier, StoreInfo>();
1203  var identifier = storeElement.GetAttributeIdentifier("identifier", "");
1204  if (identifier.IsEmpty)
1205  {
1206  // Previously saved store data (with no identifier) is discarded and new store data will be created
1207  continue;
1208  }
1209  if (StoreIdentifiers.Contains(identifier))
1210  {
1211  if (!Stores.ContainsKey(identifier))
1212  {
1213  Stores.Add(identifier, new StoreInfo(this, storeElement));
1214  }
1215  else
1216  {
1217  string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
1218  DebugConsole.ThrowError(msg);
1219  GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
1220  continue;
1221  }
1222  }
1223  else
1224  {
1225  string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
1226  DebugConsole.ThrowError(msg);
1227  GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
1228  continue;
1229  }
1230  }
1231  // Backwards compatibility: create new stores for any identifiers not present in the save data
1232  foreach (var id in StoreIdentifiers)
1233  {
1234  AddNewStore(id);
1235  }
1236  }
1237 
1238  public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this);
1239 
1243  public void RegisterTakenItems(IEnumerable<Item> items)
1244  {
1245  foreach (Item item in items)
1246  {
1247  if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; }
1248  if (item.IsSalvageMissionItem) { continue; }
1249  if (item.OriginalModuleIndex < 0)
1250  {
1251  DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost.");
1252  continue;
1253  }
1254  takenItems.Add(new TakenItem(item));
1255  }
1256  }
1257 
1261  public void RegisterKilledCharacters(IEnumerable<Character> characters)
1262  {
1263  foreach (Character character in characters)
1264  {
1265  if (character?.Info == null) { continue; }
1266  killedCharacterIdentifiers.Add(character.Info.GetIdentifier());
1267  }
1268  }
1269 
1270  public void RemoveTakenItems()
1271  {
1272  foreach (TakenItem takenItem in takenItems)
1273  {
1274  Item item = Item.ItemList.Find(it => takenItem.Matches(it));
1275  item?.Remove();
1276  }
1277  }
1278 
1279  public int GetAdjustedMechanicalCost(int cost)
1280  {
1281  float discount = 0.0f;
1282  if (Reputation != null)
1283  {
1284  discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f);
1285  }
1286  return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
1287  }
1288 
1289  public int GetAdjustedHealCost(int cost)
1290  {
1291  float discount = 0.0f;
1292  if (Reputation != null)
1293  {
1294  discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f);
1295  }
1296  return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier);
1297  }
1298 
1299  public StoreInfo GetStore(Identifier identifier)
1300  {
1301  if (Stores != null && Stores.TryGetValue(identifier, out var store))
1302  {
1303  return store;
1304  }
1305  return null;
1306  }
1307 
1309  public void CreateStores(bool force = false)
1310  {
1311  // In multiplayer, stores should be created by the server and loaded from save data by clients
1312  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
1313  if (!force && Stores != null) { return; }
1314  UpdateStoreIdentifiers();
1315  if (Stores != null)
1316  {
1317  // Remove any stores with no corresponding merchants at the location
1318  foreach (var storeIdentifier in Stores.Keys)
1319  {
1320  if (!StoreIdentifiers.Contains(storeIdentifier))
1321  {
1322  Stores.Remove(storeIdentifier);
1323  }
1324  }
1325  foreach (var identifier in StoreIdentifiers)
1326  {
1327  if (Stores.TryGetValue(identifier, out var store))
1328  {
1329  store.Balance = Math.Max(store.Balance, StoreInitialBalance);
1330  var newStock = store.CreateStock();
1331  foreach (var oldStockItem in store.Stock)
1332  {
1333  if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem)
1334  {
1335  if (oldStockItem.Quantity > newStockItem.Quantity)
1336  {
1337  newStockItem.Quantity = oldStockItem.Quantity;
1338  }
1339  }
1340  }
1341  store.Stock.Clear();
1342  store.Stock.AddRange(newStock);
1343  store.GenerateSpecials();
1344  store.GeneratePriceModifier();
1345  }
1346  else
1347  {
1348  AddNewStore(identifier);
1349  }
1350  }
1351  }
1352  else
1353  {
1354  foreach (var identifier in StoreIdentifiers)
1355  {
1356  AddNewStore(identifier);
1357  }
1358  }
1359  }
1360 
1361  public void UpdateStores()
1362  {
1363  // In multiplayer, stores should be updated by the server and loaded from save data by clients
1364  if (GameMain.NetworkMember is { IsClient: true }) { return; }
1365  if (Stores == null)
1366  {
1367  CreateStores();
1368  return;
1369  }
1370  var storesToRemove = new HashSet<Identifier>();
1371  foreach (var store in Stores.Values)
1372  {
1373  if (!StoreIdentifiers.Contains(store.Identifier))
1374  {
1375  storesToRemove.Add(store.Identifier);
1376  continue;
1377  }
1378  if (store.Balance < StoreInitialBalance)
1379  {
1380  store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
1381  }
1382  var stock = new List<PurchasedItem>(store.Stock);
1383  var stockToRemove = new List<PurchasedItem>();
1384 
1385  foreach (var itemPrefab in ItemPrefab.Prefabs)
1386  {
1387  var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
1388  if (itemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo))
1389  {
1390  if (existingStock == null)
1391  {
1392  //can be bought from the location, but not in stock - some new item added by an update or mod?
1393  stock.Add(StoreInfo.CreateInitialStockItem(itemPrefab, priceInfo));
1394  }
1395  else
1396  {
1397  existingStock.Quantity =
1398  Math.Min(
1399  existingStock.Quantity + 1,
1400  priceInfo.MaxAvailableAmount);
1401  }
1402  }
1403  else if (existingStock != null)
1404  {
1405  stockToRemove.Add(existingStock);
1406  }
1407  }
1408 
1409  stockToRemove.ForEach(i => stock.Remove(i));
1410  store.Stock.Clear();
1411  store.Stock.AddRange(stock);
1412  store.GeneratePriceModifier();
1413  }
1414 
1415  StepsSinceSpecialsUpdated++;
1416  foreach (var identifier in storesToRemove)
1417  {
1418  Stores.Remove(identifier);
1419  }
1420  foreach (var identifier in StoreIdentifiers)
1421  {
1422  AddNewStore(identifier);
1423  }
1424  }
1425 
1426  public void UpdateSpecials()
1427  {
1428  if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; }
1429 
1430  int extraSpecialSalesCount = GetExtraSpecialSalesCount();
1431 
1432  foreach (StoreInfo store in Stores.Values)
1433  {
1434  if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; }
1435 
1436  store.GenerateSpecials();
1437  }
1438  }
1439 
1440  private void UpdateStoreIdentifiers()
1441  {
1442  StoreIdentifiers.Clear();
1443  foreach (var outpostParam in OutpostGenerationParams.OutpostParams)
1444  {
1445  if (!outpostParam.AllowedLocationTypes.Contains(Type.Identifier)) { continue; }
1446  foreach (var identifier in outpostParam.GetStoreIdentifiers())
1447  {
1448  StoreIdentifiers.Add(identifier);
1449  }
1450  }
1451  }
1452 
1453  private void AddNewStore(Identifier identifier)
1454  {
1455  Stores ??= new Dictionary<Identifier, StoreInfo>();
1456  if (Stores.ContainsKey(identifier)) { return; }
1457  var newStore = new StoreInfo(this, identifier);
1458  Stores.Add(identifier, newStore);
1459  }
1460 
1461  public void AddStock(Dictionary<Identifier, List<SoldItem>> items)
1462  {
1463  if (items == null) { return; }
1464  foreach (var storeItems in items)
1465  {
1466  if (GetStore(storeItems.Key) is { } store)
1467  {
1468  store.AddStock(storeItems.Value);
1469  }
1470  }
1471  }
1472 
1473  public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
1474  {
1475  if (items == null) { return; }
1476  foreach (var storeItems in items)
1477  {
1478  if (GetStore(storeItems.Key) is { } store)
1479  {
1480  store.RemoveStock(storeItems.Value);
1481  }
1482  }
1483  }
1484 
1485  public static int GetExtraSpecialSalesCount()
1486  {
1487  var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
1488  if (!characters.Any()) { return 0; }
1489  return characters.Max(static c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
1490  }
1491 
1492  public bool CanHaveSubsForSale()
1493  {
1494  return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub);
1495  }
1496 
1497  public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined)
1498  {
1499  if (CanHaveSubsForSale())
1500  {
1502  }
1503  return 0;
1504  }
1505 
1507  {
1508  return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
1509  }
1510 
1511  private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
1512  {
1513  return LevelData != null &&
1515  LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType));
1516  }
1517 
1518  public void Reset(CampaignMode campaign)
1519  {
1521  {
1522  ChangeType(campaign, OriginalType);
1523  PendingLocationTypeChange = null;
1524  }
1525  CreateStores(force: true);
1526  ClearMissions();
1527  LevelData?.EventHistory?.Clear();
1529  }
1530 
1531  public XElement Save(Map map, XElement parentElement)
1532  {
1533  var locationElement = new XElement("location",
1534  new XAttribute("type", Type.Identifier),
1535  new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
1536  /*not used currently (we load the nameIdentifier instead),
1537  * but could make sense to include still for backwards compatibility reasons*/
1538  new XAttribute("name", DisplayName),
1539  new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
1540  new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
1541  new XAttribute("pricemultiplier", PriceMultiplier),
1542  new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
1543  new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
1544  new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
1545  new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
1546  new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
1547 
1548  if (!rawName.IsNullOrEmpty())
1549  {
1550  locationElement.Add(new XAttribute(nameof(rawName), rawName));
1551  }
1552  else
1553  {
1554  locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
1555  locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
1556  }
1557 
1558  if (Faction != null)
1559  {
1560  locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
1561  }
1562  if (SecondaryFaction != null)
1563  {
1564  locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier));
1565  }
1566 
1567  LevelData.Save(locationElement);
1568 
1569  for (int i = 0; i < Type.CanChangeTo.Count; i++)
1570  {
1571  for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
1572  {
1573  if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
1574  {
1575  locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
1576  }
1577  }
1578  }
1579 
1580  if (PendingLocationTypeChange.HasValue)
1581  {
1582  var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay));
1583  if (PendingLocationTypeChange.Value.parentMission != null)
1584  {
1585  changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
1586  locationElement.Add(changeElement);
1587  }
1588  else
1589  {
1590  int index = Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
1591  changeElement.Add(new XAttribute("index", index));
1592  if (index == -1)
1593  {
1594  DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
1595  }
1596  else
1597  {
1598  locationElement.Add(changeElement);
1599  }
1600  }
1601  }
1602 
1604  {
1605  locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
1606  }
1607 
1608  if (takenItems.Any())
1609  {
1610  locationElement.Add(new XAttribute(
1611  "takenitems",
1612  string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerIndex + ";" + it.ModuleIndex))));
1613  }
1614  if (killedCharacterIdentifiers.Any())
1615  {
1616  locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers)));
1617  }
1618 
1619  if (Stores != null)
1620  {
1621  foreach (var store in Stores.Values)
1622  {
1623  var storeElement = new XElement("store",
1624  new XAttribute("identifier", store.Identifier.Value),
1625  new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
1626  new XAttribute("balance", store.Balance),
1627  new XAttribute("pricemodifier", store.PriceModifier));
1628  foreach (PurchasedItem item in store.Stock)
1629  {
1630  if (item?.ItemPrefab == null) { continue; }
1631  storeElement.Add(new XElement("stock",
1632  new XAttribute("id", item.ItemPrefab.Identifier),
1633  new XAttribute("qty", item.Quantity)));
1634  }
1635  if (store.DailySpecials.Any())
1636  {
1637  var dailySpecialElement = new XElement("dailyspecials");
1638  foreach (var item in store.DailySpecials)
1639  {
1640  dailySpecialElement.Add(new XElement("item",
1641  new XAttribute("id", item.Identifier)));
1642  }
1643  storeElement.Add(dailySpecialElement);
1644  }
1645  if (store.RequestedGoods.Any())
1646  {
1647  var requestedGoodsElement = new XElement("requestedgoods");
1648  foreach (var item in store.RequestedGoods)
1649  {
1650  requestedGoodsElement.Add(new XElement("item",
1651  new XAttribute("id", item.Identifier)));
1652  }
1653  storeElement.Add(requestedGoodsElement);
1654  }
1655  locationElement.Add(storeElement);
1656  }
1657  }
1658 
1659  if (AvailableMissions is List<Mission> missions && missions.Any())
1660  {
1661  var missionsElement = new XElement("missions");
1662  foreach (Mission mission in missions)
1663  {
1664  var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
1665  var destinationIndex = map.Locations.IndexOf(location);
1666  var originIndex = map.Locations.IndexOf(mission.OriginLocation);
1667  missionsElement.Add(new XElement("mission",
1668  new XAttribute("prefabid", mission.Prefab.Identifier),
1669  new XAttribute("destinationindex", destinationIndex),
1670  new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted),
1671  new XAttribute("origin", originIndex),
1672  new XAttribute("selected", selectedMissions.Contains(mission))));
1673  }
1674  locationElement.Add(missionsElement);
1675  }
1676 
1677  parentElement.Add(locationElement);
1678 
1679  return locationElement;
1680  }
1681 
1682  public void Remove()
1683  {
1685  }
1686 
1687  public void RemoveProjSpecific()
1688  {
1689  HireManager?.Remove();
1690  }
1691 
1693  {
1694  public AbilityLocation(Location location)
1695  {
1696  Location = location;
1697  }
1698 
1699  public Location Location { get; set; }
1700  }
1701  }
1702 }
static readonly PrefabCollection< Biome > Prefabs
Definition: Biome.cs:10
int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType)
Definition: Biome.cs:86
bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
Stores information about the Character that is needed between rounds in the menu etc....
int GetIdentifier()
Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name,...
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
FactionPrefab Prefab
Definition: Factions.cs:18
Reputation Reputation
Definition: Factions.cs:17
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
void RemoveCharacter(CharacterInfo character)
Definition: HireManager.cs:19
void GenerateCharacters(Location location, int amount)
Definition: HireManager.cs:41
const int MaxAvailableCharacters
Definition: HireManager.cs:12
List< CharacterInfo > AvailableCharacters
Definition: HireManager.cs:9
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
PriceInfo GetPriceInfo(Location.StoreInfo store)
override ImmutableHashSet< Identifier > Tags
readonly List< Identifier > EventHistory
Events that have previously triggered in this level. Used for making events the player hasn't seen ye...
Definition: LevelData.cs:67
readonly float Difficulty
Definition: LevelData.cs:23
void Save(XElement parentElement)
Definition: LevelData.cs:334
static IEnumerable< OutpostGenerationParams > GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
Definition: LevelData.cs:315
bool OutpostGenerationParamsExist
Definition: LevelData.cs:313
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
AbilityLocation(Location location)
Definition: Location.cs:1694
int PriceModifier
In percentages. Larger values make buying more expensive and selling less profitable,...
Definition: Location.cs:122
int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerRequestedGoods=true)
Definition: Location.cs:324
void SetMerchantFaction(Identifier factionIdentifier)
Definition: Location.cs:350
List< PurchasedItem > CreateStock()
Definition: Location.cs:195
override string ToString()
Definition: Location.cs:389
StoreInfo(Location location, Identifier identifier)
Create new StoreInfo
Definition: Location.cs:134
Identifier GetMerchantOrLocationFactionIdentifier()
Definition: Location.cs:355
float GetReputationModifier(bool buying)
Definition: Location.cs:360
List< ItemPrefab > DailySpecials
Definition: Location.cs:117
StoreInfo(Location location, XElement storeElement)
Load previously saved StoreInfo
Definition: Location.cs:146
void AddStock(List< SoldItem > items)
Definition: Location.cs:206
int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerDailySpecials=true)
Definition: Location.cs:283
List< PurchasedItem > Stock
Definition: Location.cs:116
void RemoveStock(List< PurchasedItem > items)
Definition: Location.cs:224
List< ItemPrefab > RequestedGoods
Definition: Location.cs:118
static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
Definition: Location.cs:189
bool IsEqual(TakenItem obj)
Definition: Location.cs:38
readonly Identifier Identifier
Definition: Location.cs:17
readonly int OriginalContainerIndex
Definition: Location.cs:18
bool Matches(Item item)
Definition: Location.cs:43
readonly ushort OriginalID
Definition: Location.cs:15
readonly ushort ModuleIndex
Definition: Location.cs:16
TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
Definition: Location.cs:20
Location OtherLocation(Location location)
void RemoveHireableCharacter(CharacterInfo character)
Definition: Location.cs:1101
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
Definition: Location.cs:744
int GetAdjustedHealCost(int cost)
Definition: Location.cs:1289
void LoadStores(XElement locationElement)
Definition: Location.cs:1196
IEnumerable< Mission > GetMissionsInConnection(LocationConnection connection)
Definition: Location.cs:1095
void ForceName(Identifier nameId)
Definition: Location.cs:1189
int TimeSinceLastTypeChange
Definition: Location.cs:510
LocationTypeChange typeChange
Definition: Location.cs:77
void ForceHireableCharacters(IEnumerable< CharacterInfo > hireableCharacters)
Definition: Location.cs:1133
void AddStock(Dictionary< Identifier, List< SoldItem >> items)
Definition: Location.cs:1461
readonly List< LocationConnection > Connections
Definition: Location.cs:56
HashSet< Identifier > StoreIdentifiers
Definition: Location.cs:411
LocationType Type
Definition: Location.cs:91
HireManager HireManager
Definition: Location.cs:535
override string ToString()
Definition: Location.cs:537
float MechanicalPriceMultiplier
Definition: Location.cs:503
StoreInfo GetStore(Identifier identifier)
Definition: Location.cs:1299
XElement Save(Map map, XElement parentElement)
Definition: Location.cs:1531
bool IsGateBetweenBiomes
Definition: Location.cs:512
bool IsCriticallyRadiated()
Definition: Location.cs:1069
Mission UnlockMissionByTag(Identifier tag, Random random=null, ContentPackage invokingContentPackage=null)
Definition: Location.cs:868
Location(CampaignMode campaign, XElement element)
Create a location from save data
Definition: Location.cs:554
Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost=false, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
Definition: Location.cs:542
static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
Definition: Location.cs:739
IEnumerable< Mission > AvailableMissions
Definition: Location.cs:432
void LoadLocationTypeChange(XElement locationElement)
Definition: Location.cs:687
void Reset(CampaignMode campaign)
Definition: Location.cs:1518
Identifier NameIdentifier
Definition: Location.cs:60
IEnumerable< int > KilledCharacterIdentifiers
Definition: Location.cs:426
void SelectMission(Mission mission)
Definition: Location.cs:452
bool CanHaveSubsForSale()
Definition: Location.cs:1492
IEnumerable< Mission > SelectedMissions
Definition: Location.cs:442
void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
Definition: Location.cs:792
string LastTypeChangeMessage
Definition: Location.cs:508
bool LocationTypeChangesBlocked
Is some mission blocking this location from changing its type, or have location type changes been for...
Definition: Location.cs:83
void ClearMissions()
Removes all unlocked missions from the location
Definition: Location.cs:1056
void UnlockInitialMissions(Rand.RandSync randSync=Rand.RandSync.ServerAndClient)
Definition: Location.cs:816
int GetAdjustedMechanicalCost(int cost)
Definition: Location.cs:1279
Vector2 MapPosition
Definition: Location.cs:89
void RegisterTakenItems(IEnumerable< Item > items)
Mark the items that have been taken from the outpost to prevent them from spawning when re-entering t...
Definition: Location.cs:1243
bool IsSubmarineAvailable(SubmarineInfo info)
Definition: Location.cs:1506
LocationType GetLocationType()
Definition: Location.cs:1079
List< int > GetSelectedMissionIndices()
Definition: Location.cs:467
bool DisallowLocationTypeChanges
Definition: Location.cs:85
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
Definition: Location.cs:76
void UnlockMission(MissionPrefab missionPrefab)
Definition: Location.cs:835
void RemoveProjSpecific()
Definition: Location.cs:1687
void InstantiateLoadedMissions(Map map)
Definition: Location.cs:1011
void DeselectMission(Mission mission)
Definition: Location.cs:461
void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
Definition: Location.cs:828
void RemoveTakenItems()
Definition: Location.cs:1270
LocalizedString DisplayName
Definition: Location.cs:58
void RemoveStock(Dictionary< Identifier, List< PurchasedItem >> items)
Definition: Location.cs:1473
int HighestSubmarineTierAvailable(SubmarineClass submarineClass=SubmarineClass.Undefined)
Definition: Location.cs:1497
LevelData LevelData
Definition: Location.cs:95
int LocationTypeChangeCooldown
Definition: Location.cs:78
LocationType OriginalType
Definition: Location.cs:93
Faction SecondaryFaction
Definition: Location.cs:101
IEnumerable< CharacterInfo > GetHireableCharacters()
Definition: Location.cs:1117
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
Definition: Location.cs:1309
void LoadMissions(XElement locationElement)
Definition: Location.cs:723
void RegisterKilledCharacters(IEnumerable< Character > characters)
Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
Definition: Location.cs:1261
Dictionary< Identifier, StoreInfo > Stores
Definition: Location.cs:395
Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage=null)
Definition: Location.cs:842
static int GetExtraSpecialSalesCount()
Definition: Location.cs:1485
IEnumerable< TakenItem > TakenItems
Definition: Location.cs:420
void SetSelectedMissionIndices(IEnumerable< int > missionIndices)
Definition: Location.cs:480
readonly CharacterTeamType OutpostTeam
Definition: LocationType.cs:34
readonly ImmutableArray< Identifier > MissionIdentifiers
Definition: LocationType.cs:38
readonly Identifier ForceLocationName
Definition: LocationType.cs:30
IReadOnlyList< string > NameFormats
Definition: LocationType.cs:64
Identifier Faction
If set, forces the location to be assigned to this faction. Set to "None" if you don't want the locat...
Definition: LocationType.cs:92
Identifier SecondaryFaction
If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want...
Definition: LocationType.cs:97
readonly ImmutableArray< Identifier > MissionTags
Definition: LocationType.cs:39
static LocationType Random(Random rand, int? zone=null, bool requireOutpost=false, Func< LocationType, bool > predicate=null)
readonly List< LocationTypeChange > CanChangeTo
Definition: LocationType.cs:36
static readonly PrefabCollection< LocationType > Prefabs
Definition: LocationType.cs:15
int StorePriceModifierRange
In percentages
Identifier GetRandomNameId(Random rand, IEnumerable< Location > existingLocations)
string GetRandomRawName(Random rand, IEnumerable< Location > existingLocations)
For backwards compatibility. Chooses a random name from the names defined in the ....
Identifier ReplaceInRadiation
Definition: LocationType.cs:87
int OriginalModuleIndex
The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
static MapEntityPrefab FindByIdentifier(Identifier identifier)
Location OriginLocation
Where was this mission received from? Affects which faction we give reputation for if the mission is ...
static readonly PrefabCollection< MissionPrefab > Prefabs
static readonly PrefabCollection< OutpostGenerationParams > OutpostParams
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
int MinAvailableAmount
Minimum number of items available at a given store
Definition: PriceInfo.cs:16
int MaxAvailableAmount
Maximum number of items available at a given store. Defaults to 20% more than the minimum amount.
Definition: PriceInfo.cs:21
bool CanBeSpecial
Can the item be a Daily Special or a Requested Good
Definition: PriceInfo.cs:25
float??????? Value
Definition: Reputation.cs:44
const float HostileThreshold
Definition: Reputation.cs:9
float NormalizedValue
Reputation value normalized to the range of 0-1
Definition: Reputation.cs:39
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
FactionAffiliation
Definition: Factions.cs:9