3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
21 public TakenItem(
Identifier identifier, UInt16 originalID,
int originalContainerIndex, ushort moduleIndex)
31 System.Diagnostics.Debug.Assert(item.
OriginalModuleIndex >= 0,
"Trying to add a non-outpost item to a location's taken items");
57 public readonly List<LocationConnection>
Connections =
new List<LocationConnection>();
63 private int nameFormatIndex;
64 private Identifier nameIdentifier;
71 private string rawName;
119 public List<PurchasedItem>
Stock {
get; } =
new List<PurchasedItem>();
127 private float MaxReputationModifier =>
Location.StoreMaxReputationModifier;
151 Identifier = storeElement.GetAttributeIdentifier(
"identifier",
"");
154 PriceModifier = storeElement.GetAttributeInt(
"pricemodifier", 0);
156 if (storeElement.Attribute(
"stepssincespecialsupdated") !=
null)
158 location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt(
"stepssincespecialsupdated", 0);
160 foreach (var stockElement
in storeElement.GetChildElements(
"stock"))
162 var identifier = stockElement.GetAttributeIdentifier(
"id",
Identifier.Empty);
164 int qty = stockElement.GetAttributeInt(
"qty", 0);
165 if (qty < 1) {
continue; }
168 if (storeElement.GetChildElement(
"dailyspecials") is XElement specialsElement)
170 var loadedDailySpecials = LoadStoreSpecials(specialsElement);
173 if (storeElement.GetChildElement(
"requestedgoods") is XElement goodsElement)
175 var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
179 static List<ItemPrefab> LoadStoreSpecials(XElement element)
181 var specials =
new List<ItemPrefab>();
182 foreach (var childElement
in element.GetChildElements(
"item"))
184 var
id = childElement.GetAttributeIdentifier(
"id",
Identifier.Empty);
186 specials.Add(prefab);
200 var stock =
new List<PurchasedItem>();
203 if (!prefab.CanBeBoughtFrom(
this, out var priceInfo)) {
continue; }
211 if (items ==
null || items.None()) {
return; }
212 DebugConsole.NewMessage($
"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly:
true);
213 foreach (var item
in items)
215 if (
Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is
PurchasedItem stockItem)
217 stockItem.Quantity += 1;
218 DebugConsole.NewMessage($
"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly:
true);
222 DebugConsole.NewMessage($
"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly:
true);
229 if (items ==
null || items.None()) {
return; }
230 DebugConsole.NewMessage($
"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly:
true);
233 if (
Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is
PurchasedItem stockItem)
235 stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0);
236 DebugConsole.NewMessage($
"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly:
true);
243 var availableStock =
new Dictionary<ItemPrefab, float>();
244 foreach (var stockItem
in Stock)
246 if (stockItem.Quantity < 1) {
continue; }
248 if (stockItem.ItemPrefab.GetPriceInfo(
this) is
PriceInfo priceInfo)
250 if (!priceInfo.CanBeSpecial) {
continue; }
251 var baseQuantity = priceInfo.MinAvailableAmount;
252 weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
253 if (weight < 0.0f) {
continue; }
255 availableStock.Add(stockItem.ItemPrefab, weight);
261 if (availableStock.None()) {
break; }
262 var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
263 if (item ==
null) {
break; }
265 availableStock.Remove(item);
273 if (item ==
null) {
break; }
276 Location.StepsSinceSpecialsUpdated = 0;
289 if (priceInfo ==
null) {
return 0; }
290 float price = priceInfo.
Price;
293 price *= priceInfo.BuyingPriceMultiplier;
297 price =
Location.DailySpecialPriceModifier * price;
305 price *= campaign.Settings.ShopPriceMultiplier;
309 if (characters.Any())
314 price *= 1f - characters.Max(
static c => c.GetStatValue(
StatTypes.StoreBuyMultiplierAffiliated, includeSaved:
false));
315 price *= 1f - characters.Max(
static c => c.Info.GetSavedStatValue(
StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
316 price *= 1f - characters.Max(c => item.
Tags.Sum(tag => c.Info.GetSavedStatValue(
StatTypes.StoreBuyMultiplierAffiliated, tag)));
318 price *= 1f - characters.Max(
static c => c.GetStatValue(
StatTypes.StoreBuyMultiplier, includeSaved:
false));
319 price *= 1f - characters.Max(c => item.
Tags.Sum(tag => c.Info.GetSavedStatValue(
StatTypes.StoreBuyMultiplier, tag)));
322 return Math.Max((
int)price, 1);
330 if (priceInfo ==
null) {
return 0; }
331 float price =
Location.StoreSellPriceModifier * priceInfo.Price;
337 price =
Location.RequestGoodPriceModifier * price;
343 if (characters.Any())
345 price *= 1f + characters.Max(
static c => c.GetStatValue(
StatTypes.StoreSellMultiplier, includeSaved:
false));
346 price *= 1f + characters.Max(c => item.
Tags.Sum(tag => c.Info.GetSavedStatValue(
StatTypes.StoreSellMultiplier, tag)));
350 return Math.Max((
int)price, 1);
367 if (reputation ==
null) {
return 1.0f; }
370 if (reputation.Value > 0.0f)
372 return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
376 return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
381 if (reputation.Value > 0.0f)
383 return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
387 return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
398 public Dictionary<Identifier, StoreInfo>
Stores {
get;
set; }
410 private const int SpecialsUpdateInterval = 3;
413 private int StepsSinceSpecialsUpdated {
get;
set; }
418 private const float MechanicalMaxDiscountPercentage = 50.0f;
419 private const float HealMaxDiscountPercentage = 10.0f;
421 private readonly List<TakenItem> takenItems =
new List<TakenItem>();
424 get {
return takenItems; }
427 private readonly HashSet<int> killedCharacterIdentifiers =
new HashSet<int>();
430 get {
return killedCharacterIdentifiers; }
433 private readonly List<Mission> availableMissions =
new List<Mission>();
438 availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
439 return availableMissions;
443 private readonly List<Mission> selectedMissions =
new List<Mission>();
448 selectedMissions.RemoveAll(m => !availableMissions.Contains(m));
449 return selectedMissions;
459 selectedMissions.Add(mission);
460 selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
466 selectedMissions.Remove(mission);
472 List<int> selectedMissionIndices =
new List<int>();
475 if (availableMissions.Contains(mission))
477 selectedMissionIndices.Add(availableMissions.IndexOf(mission));
480 return selectedMissionIndices;
485 selectedMissions.Clear();
486 foreach (
int missionIndex
in missionIndices)
488 if (missionIndex < 0 || missionIndex >= availableMissions.Count)
490 DebugConsole.ThrowError($
"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
493 selectedMissions.Add(availableMissions[missionIndex]);
497 private float priceMultiplier = 1.0f;
500 get {
return priceMultiplier; }
501 set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
504 private float mechanicalpriceMultiplier = 1.0f;
507 get => mechanicalpriceMultiplier;
508 set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
517 private readonly
struct LoadedMission
520 public readonly
int TimesAttempted;
521 public readonly
int OriginLocationIndex;
522 public readonly
int DestinationIndex;
523 public readonly
bool SelectedMission;
525 public LoadedMission(XElement element)
527 var
id = element.GetAttributeIdentifier(
"prefabid", Identifier.Empty);
529 TimesAttempted = element.GetAttributeInt(
"timesattempted", 0);
530 OriginLocationIndex = element.GetAttributeInt(
"origin", -1);
531 DestinationIndex = element.GetAttributeInt(
"destinationindex", -1);
532 SelectedMission = element.GetAttributeBool(
"selected",
false);
536 private List<LoadedMission> loadedMissions;
542 return $
"Location ({DisplayName ?? "null"})";
545 public Location(Vector2 mapPosition,
int? zone, Random rand,
bool requireOutpost =
false,
LocationType forceLocationType =
null, IEnumerable<Location> existingLocations =
null)
548 CreateRandomName(
Type, rand, existingLocations);
550 PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
559 Identifier locationTypeId = element.GetAttributeIdentifier(
"type",
"");
560 bool typeNotFound = GetTypeOrFallback(locationTypeId, out
LocationType type);
563 Identifier originalLocationTypeId = element.GetAttributeIdentifier(
"originaltype", locationTypeId);
564 GetTypeOrFallback(originalLocationTypeId, out
LocationType originalType);
567 nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier),
"");
568 if (nameIdentifier.IsEmpty)
571 DisplayName = element.GetAttributeString(
"name",
"");
572 rawName = element.GetAttributeString(
"rawname", element.GetAttributeString(
"basename",
DisplayName.
Value));
573 nameIdentifier = rawName.ToIdentifier();
577 nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
581 MapPosition = element.GetAttributeVector2(
"position", Vector2.Zero);
587 StepsSinceSpecialsUpdated = element.GetAttributeInt(
"stepssincespecialsupdated", 0);
589 var factionIdentifier = element.GetAttributeIdentifier(
"faction", Identifier.Empty);
590 if (!factionIdentifier.IsEmpty)
592 Faction = campaign.
Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
594 var secondaryFactionIdentifier = element.GetAttributeIdentifier(
"secondaryfaction", Identifier.Empty);
595 if (!secondaryFactionIdentifier.IsEmpty)
599 Identifier biomeId = element.GetAttributeIdentifier(
"biome", Identifier.Empty);
600 if (biomeId != Identifier.Empty)
608 DebugConsole.ThrowError($
"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
625 string[] takenItemStr = element.GetAttributeStringArray(
"takenitems", Array.Empty<
string>());
626 foreach (
string takenItem
in takenItemStr)
628 string[] takenItemSplit = takenItem.Split(
';');
629 if (takenItemSplit.Length != 4)
631 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item data \"{takenItem}\"");
634 if (!ushort.TryParse(takenItemSplit[1], out ushort
id))
636 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
639 if (!
int.TryParse(takenItemSplit[2], out
int containerIndex))
641 DebugConsole.ThrowError($
"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\"");
644 if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
646 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
649 takenItems.Add(
new TakenItem(takenItemSplit[0].ToIdentifier(),
id, containerIndex, moduleIndex));
652 killedCharacterIdentifiers = element.GetAttributeIntArray(
"killedcharacters", Array.Empty<
int>()).ToHashSet();
654 System.Diagnostics.Debug.Assert(
Type !=
null, $
"Could not find the location type \"{locationTypeId}\"!");
657 LevelData =
new LevelData(element.GetChildElement(
"Level"), clampDifficultyToBiome:
true);
659 PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
664 bool GetTypeOrFallback(Identifier identifier, out
LocationType type)
669 if (identifier ==
"lair")
672 addInitialMissionsForType =
Type;
676 DebugConsole.AddWarning($
"Could not find location type \"{identifier}\". Using location type \"None\" instead.");
682 element.SetAttributeValue(
"type", type.Identifier.ToString());
694 foreach (var subElement
in locationElement.Elements())
696 switch (subElement.Name.ToString())
698 case "pendinglocationtypechange":
699 int timer = subElement.GetAttributeInt(
"timer", 0);
700 if (subElement.Attribute(
"index") !=
null)
702 int locationTypeChangeIndex = subElement.GetAttributeInt(
"index", 0);
703 if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >=
Type.
CanChangeTo.Count)
705 DebugConsole.AddWarning($
"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
708 PendingLocationTypeChange = (
Type.
CanChangeTo[locationTypeChangeIndex], timer,
null);
712 Identifier missionIdentifier = subElement.GetAttributeIdentifier(
"missionidentifier",
"");
716 DebugConsole.AddWarning($
"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
719 PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
728 if (locationElement.GetChildElement(
"missions") is XElement missionsElement)
730 loadedMissions =
new List<LoadedMission>();
731 foreach (XElement childElement
in missionsElement.GetChildElements(
"mission"))
733 var loadedMission =
new LoadedMission(childElement);
734 if (loadedMission.MissionPrefab !=
null)
736 loadedMissions.Add(loadedMission);
742 public static Location CreateRandom(Vector2 position,
int? zone, Random rand,
bool requireOutpost,
LocationType forceLocationType =
null, IEnumerable<Location> existingLocations =
null)
744 return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
749 if (newType ==
Type) {
return; }
753 DebugConsole.ThrowError($
"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
760 DebugConsole.Log($
"Location {rawName} changed it's type from {Type} to {newType}");
768 DebugConsole.Log($
"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
771 TextManager.Get(nameIdentifier) :
797 if (campaign ==
null) {
return; }
807 Faction TryFindFaction(Identifier identifier)
809 var faction = campaign.
GetFaction(identifier);
812 DebugConsole.ThrowError($
"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
835 AddMission(InstantiateMission(missionPrefab, connection));
842 AddMission(InstantiateMission(missionPrefab));
847 if (
AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) {
return null; }
848 if (
AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) {
return null; }
851 if (missionPrefab ==
null)
853 DebugConsole.ThrowError($
"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.",
854 contentPackage: invokingContentPackage);
860 if (
AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
865 DebugConsole.NewMessage($
"Unlocked a mission by \"{identifier}\".", debugOnly:
true);
873 if (
AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) {
return null; }
875 if (matchingMissions.None())
877 DebugConsole.ThrowError($
"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage);
881 var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.
Prefab == m));
882 if (unusedMissions.Any())
884 var suitableMissions = unusedMissions.Where(m =>
Connections.Any(c => m.IsAllowed(
this, c.OtherLocation(
this)) || m.IsAllowed(
this,
this)));
885 if (suitableMissions.None())
887 suitableMissions = unusedMissions;
890 if (filteredMissions.None())
892 DebugConsole.AddWarning($
"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.",
893 contentPackage: invokingContentPackage);
897 suitableMissions = filteredMissions;
901 ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) :
902 ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced);
906 if (
AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
911 DebugConsole.NewMessage($
"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly:
true);
916 DebugConsole.AddWarning($
"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.",
917 contentPackage: invokingContentPackage);
924 private void AddMission(
Mission mission)
928 availableMissions.Clear();
930 availableMissions.Add(mission);
932 GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
934 (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
938 private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
940 if (prefab.IsAllowed(
this,
this))
943 return InstantiateMission(prefab);
946 var suitableConnections =
Connections.Where(c => prefab.IsAllowed(
this, c.OtherLocation(
this)));
947 if (suitableConnections.None())
952 connection = ToolBox.SelectWeightedRandom(
953 suitableConnections.ToList(),
954 suitableConnections.Select(c => GetConnectionWeight(
this, c)).ToList(),
955 Rand.RandSync.Unsynced);
957 static float GetConnectionWeight(
Location location, LocationConnection c)
959 Location destination = c.OtherLocation(location);
960 if (destination ==
null) {
return 0; }
961 float minWeight = 0.0001f;
962 float lowWeight = 0.2f;
963 float normalWeight = 1.0f;
964 float maxWeight = 2.0f;
965 float weight = c.Passed ? lowWeight : normalWeight;
966 if (location.Biome.AllowedZones.Contains(1))
969 float diff = destination.MapPosition.X - location.MapPosition.X;
976 float maxRelevantDiff = 300;
977 weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
980 else if (destination.MapPosition.X > location.MapPosition.X)
984 int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
985 if (missionCount > 0)
987 weight /= missionCount * 2;
989 if (destination.IsRadiated())
993 return MathHelper.Clamp(weight, minWeight, maxWeight);
996 return InstantiateMission(prefab, connection);
999 private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
1001 Location destination = connection.OtherLocation(
this);
1002 var mission = prefab.Instantiate(
new Location[] {
this, destination },
Submarine.MainSub);
1007 private Mission InstantiateMission(MissionPrefab prefab)
1009 var mission = prefab.Instantiate(
new Location[] {
this,
this },
Submarine.MainSub);
1016 availableMissions.Clear();
1017 selectedMissions.Clear();
1018 if (loadedMissions !=
null && loadedMissions.Any())
1020 foreach (LoadedMission loadedMission
in loadedMissions)
1023 if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.
Locations.Count)
1025 destination = map.
Locations[loadedMission.DestinationIndex];
1029 destination =
Connections.First().OtherLocation(
this);
1032 if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.
Locations.Count)
1034 mission.OriginLocation = map.
Locations[loadedMission.OriginLocationIndex];
1036 mission.TimesAttempted = loadedMission.TimesAttempted;
1037 availableMissions.Add(mission);
1038 if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
1040 loadedMissions =
null;
1042 if (addInitialMissionsForType !=
null)
1052 addInitialMissionsForType =
null;
1061 availableMissions.Clear();
1062 selectedMissions.Clear();
1088 return newLocationType;
1092 DebugConsole.ThrowError($
"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found.");
1100 System.Diagnostics.Debug.Assert(
Connections.Contains(connection));
1108 DebugConsole.ThrowErrorLocalized(
"Cannot hire a character from location \"" +
DisplayName +
"\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
1113 DebugConsole.ThrowErrorLocalized(
"Cannot hire a character from location \"" +
DisplayName +
"\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
1142 private void CreateRandomName(
LocationType type, Random rand, IEnumerable<Location> existingLocations)
1151 if (nameIdentifier.IsEmpty)
1155 if (rawName.IsNullOrEmpty())
1157 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.");
1160 nameIdentifier = rawName.ToIdentifier();
1167 nameFormatIndex = rand.Next() % type.
NameFormats.Count;
1178 nameFormatIndex = rand.Next() % type.
NameFormats.Count;
1187 return GetName(locationType, nameFormatIndex, nameId);
1191 DebugConsole.ThrowError($
"Could not find the location type {locationTypeIdentifier}.\n" + Environment.StackTrace.CleanUpPath());
1198 if (type?.NameFormats ==
null || !type.
NameFormats.Any() || nameFormatIndex < 0)
1200 return TextManager.Get(nameId);
1202 return type.
NameFormats[nameFormatIndex % type.
NameFormats.Count].Replace(
"[name]", TextManager.Get(nameId).Value);
1207 rawName =
string.Empty;
1208 nameIdentifier = nameId;
1214 UpdateStoreIdentifiers();
1216 foreach (var storeElement
in locationElement.GetChildElements(
"store"))
1218 Stores ??=
new Dictionary<Identifier, StoreInfo>();
1219 var identifier = storeElement.GetAttributeIdentifier(
"identifier",
"");
1220 if (identifier.IsEmpty)
1227 if (!
Stores.ContainsKey(identifier))
1233 string msg = $
"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
1234 DebugConsole.ThrowError(msg);
1235 GameAnalyticsManager.AddErrorEventOnce(
"Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
1241 string msg = $
"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
1242 DebugConsole.ThrowError(msg);
1243 GameAnalyticsManager.AddErrorEventOnce(
"Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
1261 foreach (
Item item
in items)
1263 if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.
ID)) {
continue; }
1267 DebugConsole.ThrowError(
"Tried to register a non-outpost item as being taken from the outpost.");
1279 foreach (
Character character
in characters)
1281 if (character?.Info ==
null) {
continue; }
1288 foreach (
TakenItem takenItem
in takenItems)
1297 float discount = 0.0f;
1307 float discount = 0.0f;
1312 return (
int) Math.Ceiling((1.0f - discount) * cost *
PriceMultiplier);
1317 if (
Stores !=
null &&
Stores.TryGetValue(identifier, out var store))
1329 if (!force &&
Stores !=
null) {
return; }
1330 UpdateStoreIdentifiers();
1334 foreach (var storeIdentifier
in Stores.Keys)
1338 Stores.Remove(storeIdentifier);
1343 if (
Stores.TryGetValue(identifier, out var store))
1346 var newStock = store.CreateStock();
1347 foreach (var oldStockItem
in store.Stock)
1349 if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem)
1351 if (oldStockItem.Quantity > newStockItem.Quantity)
1353 newStockItem.Quantity = oldStockItem.Quantity;
1357 store.Stock.Clear();
1358 store.Stock.AddRange(newStock);
1359 store.GenerateSpecials();
1360 store.GeneratePriceModifier();
1364 AddNewStore(identifier);
1372 AddNewStore(identifier);
1386 var storesToRemove =
new HashSet<Identifier>();
1387 foreach (var store
in Stores.Values)
1391 storesToRemove.Add(store.Identifier);
1398 var stock =
new List<PurchasedItem>(store.Stock);
1399 var stockToRemove =
new List<PurchasedItem>();
1403 var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
1404 if (itemPrefab.CanBeBoughtFrom(store, out
PriceInfo priceInfo))
1406 if (existingStock ==
null)
1413 existingStock.Quantity =
1415 existingStock.Quantity + 1,
1416 priceInfo.MaxAvailableAmount);
1419 else if (existingStock !=
null)
1421 stockToRemove.Add(existingStock);
1425 stockToRemove.ForEach(i => stock.Remove(i));
1426 store.Stock.Clear();
1427 store.Stock.AddRange(stock);
1428 store.GeneratePriceModifier();
1431 StepsSinceSpecialsUpdated++;
1432 foreach (var identifier
in storesToRemove)
1434 Stores.Remove(identifier);
1438 AddNewStore(identifier);
1456 private void UpdateStoreIdentifiers()
1461 if (!outpostParam.AllowedLocationTypes.Contains(
Type.
Identifier)) {
continue; }
1462 foreach (var identifier
in outpostParam.GetStoreIdentifiers())
1469 private void AddNewStore(Identifier identifier)
1471 Stores ??=
new Dictionary<Identifier, StoreInfo>();
1472 if (
Stores.ContainsKey(identifier)) {
return; }
1473 var newStore =
new StoreInfo(
this, identifier);
1474 Stores.Add(identifier, newStore);
1477 public void AddStock(Dictionary<Identifier, List<SoldItem>> items)
1479 if (items ==
null) {
return; }
1480 foreach (var storeItems
in items)
1482 if (
GetStore(storeItems.Key) is { } store)
1484 store.AddStock(storeItems.Value);
1489 public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
1491 if (items ==
null) {
return; }
1492 foreach (var storeItems
in items)
1494 if (
GetStore(storeItems.Key) is { } store)
1496 store.RemoveStock(storeItems.Value);
1504 if (!characters.Any()) {
return 0; }
1505 return characters.Max(
static c => (
int)c.GetStatValue(
StatTypes.ExtraSpecialSalesCount));
1539 PendingLocationTypeChange =
null;
1547 public XElement
Save(
Map map, XElement parentElement)
1549 var locationElement =
new XElement(
"location",
1555 new XAttribute(
"biome",
Biome?.Identifier.Value ??
string.Empty),
1556 new XAttribute(
"position", XMLExtensions.Vector2ToString(
MapPosition)),
1562 new XAttribute(
"stepssincespecialsupdated", StepsSinceSpecialsUpdated));
1564 if (!rawName.IsNullOrEmpty())
1566 locationElement.Add(
new XAttribute(nameof(rawName), rawName));
1570 locationElement.Add(
new XAttribute(nameof(nameIdentifier), nameIdentifier));
1571 locationElement.Add(
new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
1576 locationElement.Add(
new XAttribute(
"faction",
Faction.
Prefab.Identifier));
1596 if (PendingLocationTypeChange.HasValue)
1598 var changeElement =
new XElement(
"pendinglocationtypechange",
new XAttribute(
"timer", PendingLocationTypeChange.Value.delay));
1599 if (PendingLocationTypeChange.Value.parentMission !=
null)
1601 changeElement.Add(
new XAttribute(
"missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
1602 locationElement.Add(changeElement);
1606 int index =
Type.
CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
1607 changeElement.Add(
new XAttribute(
"index", index));
1610 DebugConsole.AddWarning($
"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
1614 locationElement.Add(changeElement);
1624 if (takenItems.Any())
1626 locationElement.Add(
new XAttribute(
1628 string.Join(
',', takenItems.Select(it => it.Identifier +
";" + it.OriginalID +
";" + it.OriginalContainerIndex +
";" + it.ModuleIndex))));
1630 if (killedCharacterIdentifiers.Any())
1632 locationElement.Add(
new XAttribute(
"killedcharacters",
string.Join(
',', killedCharacterIdentifiers)));
1637 foreach (var store
in Stores.Values)
1639 var storeElement =
new XElement(
"store",
1640 new XAttribute(
"identifier", store.Identifier.Value),
1641 new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction),
1642 new XAttribute(
"balance", store.Balance),
1643 new XAttribute(
"pricemodifier", store.PriceModifier));
1647 storeElement.Add(
new XElement(
"stock",
1649 new XAttribute(
"qty", item.
Quantity)));
1651 if (store.DailySpecials.Any())
1653 var dailySpecialElement =
new XElement(
"dailyspecials");
1654 foreach (var item
in store.DailySpecials)
1656 dailySpecialElement.Add(
new XElement(
"item",
1657 new XAttribute(
"id", item.Identifier)));
1659 storeElement.Add(dailySpecialElement);
1661 if (store.RequestedGoods.Any())
1663 var requestedGoodsElement =
new XElement(
"requestedgoods");
1664 foreach (var item
in store.RequestedGoods)
1666 requestedGoodsElement.Add(
new XElement(
"item",
1667 new XAttribute(
"id", item.Identifier)));
1669 storeElement.Add(requestedGoodsElement);
1671 locationElement.Add(storeElement);
1677 var missionsElement =
new XElement(
"missions");
1678 foreach (
Mission mission
in missions)
1680 var location = mission.
Locations.All(l => l ==
this) ? this : mission.
Locations.FirstOrDefault(l => l !=
this);
1681 var destinationIndex = map.
Locations.IndexOf(location);
1683 missionsElement.Add(
new XElement(
"mission",
1685 new XAttribute(
"destinationindex", destinationIndex),
1687 new XAttribute(
"origin", originIndex),
1688 new XAttribute(
"selected", selectedMissions.Contains(mission))));
1690 locationElement.Add(missionsElement);
1693 parentElement.Add(locationElement);
1695 return locationElement;
static readonly PrefabCollection< Biome > Prefabs
int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType)
bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
Faction GetFaction(Identifier identifier)
FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
IReadOnlyList< Faction > Factions
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
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
static GameSession?? GameSession
static NetworkMember NetworkMember
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
void RemoveCharacter(CharacterInfo character)
void GenerateCharacters(Location location, int amount)
const int MaxAvailableCharacters
List< CharacterInfo > AvailableCharacters
static readonly List< Item > ItemList
bool IsSalvageMissionItem
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...
readonly float Difficulty
void Save(XElement parentElement)
static IEnumerable< OutpostGenerationParams > GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
bool OutpostGenerationParamsExist
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
AbilityLocation(Location location)
int PriceModifier
In percentages. Larger values make buying more expensive and selling less profitable,...
int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerRequestedGoods=true)
void SetMerchantFaction(Identifier factionIdentifier)
List< PurchasedItem > CreateStock()
override string ToString()
StoreInfo(Location location, Identifier identifier)
Create new StoreInfo
Identifier GetMerchantOrLocationFactionIdentifier()
float GetReputationModifier(bool buying)
List< ItemPrefab > DailySpecials
StoreInfo(Location location, XElement storeElement)
Load previously saved StoreInfo
void GeneratePriceModifier()
void AddStock(List< SoldItem > items)
Identifier MerchantFaction
int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo=null, bool considerDailySpecials=true)
List< PurchasedItem > Stock
void RemoveStock(List< PurchasedItem > items)
List< ItemPrefab > RequestedGoods
static PurchasedItem CreateInitialStockItem(ItemPrefab itemPrefab, PriceInfo priceInfo)
bool IsEqual(TakenItem obj)
readonly Identifier Identifier
readonly int OriginalContainerIndex
readonly ushort OriginalID
readonly ushort ModuleIndex
TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex)
Location OtherLocation(Location location)
void RemoveHireableCharacter(CharacterInfo character)
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
int GetAdjustedHealCost(int cost)
void LoadStores(XElement locationElement)
IEnumerable< Mission > GetMissionsInConnection(LocationConnection connection)
void ForceName(Identifier nameId)
int TimeSinceLastTypeChange
LocationTypeChange typeChange
void ForceHireableCharacters(IEnumerable< CharacterInfo > hireableCharacters)
void AddStock(Dictionary< Identifier, List< SoldItem >> items)
readonly List< LocationConnection > Connections
HashSet< Identifier > StoreIdentifiers
override string ToString()
float MechanicalPriceMultiplier
StoreInfo GetStore(Identifier identifier)
XElement Save(Map map, XElement parentElement)
bool IsCriticallyRadiated()
Mission UnlockMissionByTag(Identifier tag, Random random=null, ContentPackage invokingContentPackage=null)
static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
Location(CampaignMode campaign, XElement element)
Create a location from save data
Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost=false, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
IEnumerable< Mission > AvailableMissions
void LoadLocationTypeChange(XElement locationElement)
void Reset(CampaignMode campaign)
Identifier NameIdentifier
IEnumerable< int > KilledCharacterIdentifiers
void SelectMission(Mission mission)
bool CanHaveSubsForSale()
IEnumerable< Mission > SelectedMissions
void TryAssignFactionBasedOnLocationType(CampaignMode campaign)
string LastTypeChangeMessage
bool LocationTypeChangesBlocked
Is some mission blocking this location from changing its type, or have location type changes been for...
void ClearMissions()
Removes all unlocked missions from the location
void UnlockInitialMissions(Rand.RandSync randSync=Rand.RandSync.ServerAndClient)
int GetAdjustedMechanicalCost(int cost)
void RegisterTakenItems(IEnumerable< Item > items)
Mark the items that have been taken from the outpost to prevent them from spawning when re-entering t...
bool IsSubmarineAvailable(SubmarineInfo info)
LocationType GetLocationType()
List< int > GetSelectedMissionIndices()
bool DisallowLocationTypeChanges
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
void UnlockMission(MissionPrefab missionPrefab)
void RemoveProjSpecific()
void InstantiateLoadedMissions(Map map)
void DeselectMission(Mission mission)
void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
LocalizedString DisplayName
void RemoveStock(Dictionary< Identifier, List< PurchasedItem >> items)
int HighestSubmarineTierAvailable(SubmarineClass submarineClass=SubmarineClass.Undefined)
int LocationTypeChangeCooldown
LocationType OriginalType
IEnumerable< CharacterInfo > GetHireableCharacters()
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
void LoadMissions(XElement locationElement)
void RegisterKilledCharacters(IEnumerable< Character > characters)
Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
Dictionary< Identifier, StoreInfo > Stores
Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage=null)
static int GetExtraSpecialSalesCount()
static LocalizedString GetName(Identifier locationTypeIdentifier, int nameFormatIndex, Identifier nameId)
IEnumerable< TakenItem > TakenItems
void SetSelectedMissionIndices(IEnumerable< int > missionIndices)
readonly CharacterTeamType OutpostTeam
float RequestGoodPriceModifier
readonly ImmutableArray< Identifier > MissionIdentifiers
readonly Identifier ForceLocationName
IReadOnlyList< string > NameFormats
Identifier Faction
If set, forces the location to be assigned to this faction. Set to "None" if you don't want the locat...
Identifier SecondaryFaction
If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want...
float StoreSellPriceModifier
readonly ImmutableArray< Identifier > MissionTags
static LocationType Random(Random rand, int? zone=null, bool requireOutpost=false, Func< LocationType, bool > predicate=null)
readonly List< LocationTypeChange > CanChangeTo
float StoreMaxReputationModifier
static readonly PrefabCollection< LocationType > Prefabs
int StorePriceModifierRange
In percentages
float DailySpecialPriceModifier
Identifier GetRandomNameId(Random rand, IEnumerable< Location > existingLocations)
bool HasHireableCharacters
string GetRandomRawName(Random rand, IEnumerable< Location > existingLocations)
For backwards compatibility. Chooses a random name from the names defined in the ....
Identifier ReplaceInRadiation
int OriginalModuleIndex
The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
int OriginalContainerIndex
static MapEntityPrefab FindByIdentifier(Identifier identifier)
bool IsDiscovered(Location location)
List< Location > Locations
bool IsVisited(Location location)
virtual void AdjustLevelData(LevelData levelData)
readonly MissionPrefab Prefab
Location OriginLocation
Where was this mission received from? Affects which faction we give reputation for if the mission is ...
readonly Location[] Locations
static readonly PrefabCollection< MissionPrefab > Prefabs
readonly bool AllowOtherMissionsInLevel
static readonly PrefabCollection< OutpostGenerationParams > OutpostParams
ContentPackage? ContentPackage
readonly Identifier Identifier
int MinAvailableAmount
Minimum number of items available at a given store
int MaxAvailableAmount
Maximum number of items available at a given store. Defaults to 20% more than the minimum amount.
bool CanBeSpecial
Can the item be a Daily Special or a Requested Good
const float HostileThreshold
float NormalizedValue
Reputation value normalized to the range of 0-1
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.