3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
20 public TakenItem(
Identifier identifier, UInt16 originalID,
int originalContainerIndex, ushort moduleIndex)
30 System.Diagnostics.Debug.Assert(item.
OriginalModuleIndex >= 0,
"Trying to add a non-outpost item to a location's taken items");
56 public readonly List<LocationConnection>
Connections =
new List<LocationConnection>();
62 private int nameFormatIndex;
63 private Identifier nameIdentifier;
68 private string rawName;
116 public List<PurchasedItem>
Stock {
get; } =
new List<PurchasedItem>();
124 private float MaxReputationModifier =>
Location.StoreMaxReputationModifier;
148 Identifier = storeElement.GetAttributeIdentifier(
"identifier",
"");
151 PriceModifier = storeElement.GetAttributeInt(
"pricemodifier", 0);
153 if (storeElement.Attribute(
"stepssincespecialsupdated") !=
null)
155 location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt(
"stepssincespecialsupdated", 0);
157 foreach (var stockElement
in storeElement.GetChildElements(
"stock"))
159 var identifier = stockElement.GetAttributeIdentifier(
"id",
Identifier.Empty);
161 int qty = stockElement.GetAttributeInt(
"qty", 0);
162 if (qty < 1) {
continue; }
165 if (storeElement.GetChildElement(
"dailyspecials") is XElement specialsElement)
167 var loadedDailySpecials = LoadStoreSpecials(specialsElement);
170 if (storeElement.GetChildElement(
"requestedgoods") is XElement goodsElement)
172 var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
176 static List<ItemPrefab> LoadStoreSpecials(XElement element)
178 var specials =
new List<ItemPrefab>();
179 foreach (var childElement
in element.GetChildElements(
"item"))
181 var
id = childElement.GetAttributeIdentifier(
"id",
Identifier.Empty);
183 specials.Add(prefab);
197 var stock =
new List<PurchasedItem>();
200 if (!prefab.CanBeBoughtFrom(
this, out var priceInfo)) {
continue; }
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)
212 if (
Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is
PurchasedItem stockItem)
214 stockItem.Quantity += 1;
215 DebugConsole.NewMessage($
"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly:
true);
219 DebugConsole.NewMessage($
"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly:
true);
226 if (items ==
null || items.None()) {
return; }
227 DebugConsole.NewMessage($
"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly:
true);
230 if (
Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is
PurchasedItem stockItem)
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);
240 var availableStock =
new Dictionary<ItemPrefab, float>();
241 foreach (var stockItem
in Stock)
243 if (stockItem.Quantity < 1) {
continue; }
245 if (stockItem.ItemPrefab.GetPriceInfo(
this) is
PriceInfo priceInfo)
247 if (!priceInfo.CanBeSpecial) {
continue; }
248 var baseQuantity = priceInfo.MinAvailableAmount;
249 weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
250 if (weight < 0.0f) {
continue; }
252 availableStock.Add(stockItem.ItemPrefab, weight);
258 if (availableStock.None()) {
break; }
259 var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
260 if (item ==
null) {
break; }
262 availableStock.Remove(item);
270 if (item ==
null) {
break; }
273 Location.StepsSinceSpecialsUpdated = 0;
286 if (priceInfo ==
null) {
return 0; }
287 float price = priceInfo.
Price;
290 price *= priceInfo.BuyingPriceMultiplier;
294 price =
Location.DailySpecialPriceModifier * price;
302 price *= campaign.Settings.ShopPriceMultiplier;
306 if (characters.Any())
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)));
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)));
319 return Math.Max((
int)price, 1);
327 if (priceInfo ==
null) {
return 0; }
328 float price =
Location.StoreSellPriceModifier * priceInfo.Price;
334 price =
Location.RequestGoodPriceModifier * price;
340 if (characters.Any())
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)));
347 return Math.Max((
int)price, 1);
364 if (reputation ==
null) {
return 1.0f; }
367 if (reputation.Value > 0.0f)
369 return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation);
373 return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation);
378 if (reputation.Value > 0.0f)
380 return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation);
384 return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation);
395 public Dictionary<Identifier, StoreInfo>
Stores {
get;
set; }
407 private const int SpecialsUpdateInterval = 3;
410 private int StepsSinceSpecialsUpdated {
get;
set; }
415 private const float MechanicalMaxDiscountPercentage = 50.0f;
416 private const float HealMaxDiscountPercentage = 10.0f;
418 private readonly List<TakenItem> takenItems =
new List<TakenItem>();
421 get {
return takenItems; }
424 private readonly HashSet<int> killedCharacterIdentifiers =
new HashSet<int>();
427 get {
return killedCharacterIdentifiers; }
430 private readonly List<Mission> availableMissions =
new List<Mission>();
435 availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
436 return availableMissions;
440 private readonly List<Mission> selectedMissions =
new List<Mission>();
445 selectedMissions.RemoveAll(m => !availableMissions.Contains(m));
446 return selectedMissions;
456 selectedMissions.Add(mission);
457 selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
463 selectedMissions.Remove(mission);
469 List<int> selectedMissionIndices =
new List<int>();
472 if (availableMissions.Contains(mission))
474 selectedMissionIndices.Add(availableMissions.IndexOf(mission));
477 return selectedMissionIndices;
482 selectedMissions.Clear();
483 foreach (
int missionIndex
in missionIndices)
485 if (missionIndex < 0 || missionIndex >= availableMissions.Count)
487 DebugConsole.ThrowError($
"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
490 selectedMissions.Add(availableMissions[missionIndex]);
494 private float priceMultiplier = 1.0f;
497 get {
return priceMultiplier; }
498 set { priceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f); }
501 private float mechanicalpriceMultiplier = 1.0f;
504 get => mechanicalpriceMultiplier;
505 set => mechanicalpriceMultiplier = MathHelper.Clamp(value, 0.1f, 10.0f);
514 private readonly
struct LoadedMission
517 public readonly
int TimesAttempted;
518 public readonly
int OriginLocationIndex;
519 public readonly
int DestinationIndex;
520 public readonly
bool SelectedMission;
522 public LoadedMission(XElement element)
524 var
id = element.GetAttributeIdentifier(
"prefabid", Identifier.Empty);
526 TimesAttempted = element.GetAttributeInt(
"timesattempted", 0);
527 OriginLocationIndex = element.GetAttributeInt(
"origin", -1);
528 DestinationIndex = element.GetAttributeInt(
"destinationindex", -1);
529 SelectedMission = element.GetAttributeBool(
"selected",
false);
533 private List<LoadedMission> loadedMissions;
539 return $
"Location ({DisplayName ?? "null"})";
542 public Location(Vector2 mapPosition,
int? zone, Random rand,
bool requireOutpost =
false,
LocationType forceLocationType =
null, IEnumerable<Location> existingLocations =
null)
545 CreateRandomName(
Type, rand, existingLocations);
547 PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
556 Identifier locationTypeId = element.GetAttributeIdentifier(
"type",
"");
557 bool typeNotFound = GetTypeOrFallback(locationTypeId, out
LocationType type);
560 Identifier originalLocationTypeId = element.GetAttributeIdentifier(
"originaltype", locationTypeId);
561 GetTypeOrFallback(originalLocationTypeId, out
LocationType originalType);
564 nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier),
"");
565 if (nameIdentifier.IsEmpty)
568 DisplayName = element.GetAttributeString(
"name",
"");
569 rawName = element.GetAttributeString(
"rawname", element.GetAttributeString(
"basename",
DisplayName.
Value));
570 nameIdentifier = rawName.ToIdentifier();
574 nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
578 MapPosition = element.GetAttributeVector2(
"position", Vector2.Zero);
584 StepsSinceSpecialsUpdated = element.GetAttributeInt(
"stepssincespecialsupdated", 0);
586 var factionIdentifier = element.GetAttributeIdentifier(
"faction", Identifier.Empty);
587 if (!factionIdentifier.IsEmpty)
589 Faction = campaign.
Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
591 var secondaryFactionIdentifier = element.GetAttributeIdentifier(
"secondaryfaction", Identifier.Empty);
592 if (!secondaryFactionIdentifier.IsEmpty)
596 Identifier biomeId = element.GetAttributeIdentifier(
"biome", Identifier.Empty);
597 if (biomeId != Identifier.Empty)
605 DebugConsole.ThrowError($
"Error while loading the campaign map: could not find a biome with the identifier \"{biomeId}\".");
622 string[] takenItemStr = element.GetAttributeStringArray(
"takenitems", Array.Empty<
string>());
623 foreach (
string takenItem
in takenItemStr)
625 string[] takenItemSplit = takenItem.Split(
';');
626 if (takenItemSplit.Length != 4)
628 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item data \"{takenItem}\"");
631 if (!ushort.TryParse(takenItemSplit[1], out ushort
id))
633 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\"");
636 if (!
int.TryParse(takenItemSplit[2], out
int containerIndex))
638 DebugConsole.ThrowError($
"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\"");
641 if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex))
643 DebugConsole.ThrowError($
"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\"");
646 takenItems.Add(
new TakenItem(takenItemSplit[0].ToIdentifier(),
id, containerIndex, moduleIndex));
649 killedCharacterIdentifiers = element.GetAttributeIntArray(
"killedcharacters", Array.Empty<
int>()).ToHashSet();
651 System.Diagnostics.Debug.Assert(
Type !=
null, $
"Could not find the location type \"{locationTypeId}\"!");
654 LevelData =
new LevelData(element.GetChildElement(
"Level"), clampDifficultyToBiome:
true);
656 PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
661 bool GetTypeOrFallback(Identifier identifier, out
LocationType type)
666 if (identifier ==
"lair")
669 addInitialMissionsForType =
Type;
673 DebugConsole.AddWarning($
"Could not find location type \"{identifier}\". Using location type \"None\" instead.");
679 element.SetAttributeValue(
"type", type.Identifier.ToString());
691 foreach (var subElement
in locationElement.Elements())
693 switch (subElement.Name.ToString())
695 case "pendinglocationtypechange":
696 int timer = subElement.GetAttributeInt(
"timer", 0);
697 if (subElement.Attribute(
"index") !=
null)
699 int locationTypeChangeIndex = subElement.GetAttributeInt(
"index", 0);
700 if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >=
Type.
CanChangeTo.Count)
702 DebugConsole.AddWarning($
"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
705 PendingLocationTypeChange = (
Type.
CanChangeTo[locationTypeChangeIndex], timer,
null);
709 Identifier missionIdentifier = subElement.GetAttributeIdentifier(
"missionidentifier",
"");
713 DebugConsole.AddWarning($
"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
716 PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
725 if (locationElement.GetChildElement(
"missions") is XElement missionsElement)
727 loadedMissions =
new List<LoadedMission>();
728 foreach (XElement childElement
in missionsElement.GetChildElements(
"mission"))
730 var loadedMission =
new LoadedMission(childElement);
731 if (loadedMission.MissionPrefab !=
null)
733 loadedMissions.Add(loadedMission);
739 public static Location CreateRandom(Vector2 position,
int? zone, Random rand,
bool requireOutpost,
LocationType forceLocationType =
null, IEnumerable<Location> existingLocations =
null)
741 return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
746 if (newType ==
Type) {
return; }
750 DebugConsole.ThrowError($
"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
757 DebugConsole.Log($
"Location {rawName} changed it's type from {Type} to {newType}");
765 DebugConsole.Log($
"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
768 TextManager.Get(nameIdentifier) :
794 if (campaign ==
null) {
return; }
804 Faction TryFindFaction(Identifier identifier)
806 var faction = campaign.
GetFaction(identifier);
809 DebugConsole.ThrowError($
"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".",
832 AddMission(InstantiateMission(missionPrefab, connection));
839 AddMission(InstantiateMission(missionPrefab));
844 if (
AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) {
return null; }
845 if (
AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) {
return null; }
848 if (missionPrefab ==
null)
850 DebugConsole.ThrowError($
"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.",
851 contentPackage: invokingContentPackage);
857 if (
AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
862 DebugConsole.NewMessage($
"Unlocked a mission by \"{identifier}\".", debugOnly:
true);
870 if (
AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) {
return null; }
872 if (matchingMissions.None())
874 DebugConsole.ThrowError($
"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage);
878 var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.
Prefab == m));
879 if (unusedMissions.Any())
881 var suitableMissions = unusedMissions.Where(m =>
Connections.Any(c => m.IsAllowed(
this, c.OtherLocation(
this)) || m.IsAllowed(
this,
this)));
882 if (suitableMissions.None())
884 suitableMissions = unusedMissions;
887 if (filteredMissions.None())
889 DebugConsole.AddWarning($
"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.",
890 contentPackage: invokingContentPackage);
894 suitableMissions = filteredMissions;
898 ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, random) :
899 ToolBox.SelectWeightedRandom(suitableMissions, m => m.Commonness, Rand.RandSync.Unsynced);
903 if (
AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
908 DebugConsole.NewMessage($
"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly:
true);
913 DebugConsole.AddWarning($
"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.",
914 contentPackage: invokingContentPackage);
921 private void AddMission(
Mission mission)
925 availableMissions.Clear();
927 availableMissions.Add(mission);
929 GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
931 (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
935 private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
937 if (prefab.IsAllowed(
this,
this))
940 return InstantiateMission(prefab);
943 var suitableConnections =
Connections.Where(c => prefab.IsAllowed(
this, c.OtherLocation(
this)));
944 if (suitableConnections.None())
949 connection = ToolBox.SelectWeightedRandom(
950 suitableConnections.ToList(),
951 suitableConnections.Select(c => GetConnectionWeight(
this, c)).ToList(),
952 Rand.RandSync.Unsynced);
954 static float GetConnectionWeight(
Location location, LocationConnection c)
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))
966 float diff = destination.MapPosition.X - location.MapPosition.X;
973 float maxRelevantDiff = 300;
974 weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
977 else if (destination.MapPosition.X > location.MapPosition.X)
981 int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination));
982 if (missionCount > 0)
984 weight /= missionCount * 2;
986 if (destination.IsRadiated())
990 return MathHelper.Clamp(weight, minWeight, maxWeight);
993 return InstantiateMission(prefab, connection);
996 private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
998 Location destination = connection.OtherLocation(
this);
999 var mission = prefab.Instantiate(
new Location[] {
this, destination },
Submarine.MainSub);
1004 private Mission InstantiateMission(MissionPrefab prefab)
1006 var mission = prefab.Instantiate(
new Location[] {
this,
this },
Submarine.MainSub);
1013 availableMissions.Clear();
1014 selectedMissions.Clear();
1015 if (loadedMissions !=
null && loadedMissions.Any())
1017 foreach (LoadedMission loadedMission
in loadedMissions)
1020 if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.
Locations.Count)
1022 destination = map.
Locations[loadedMission.DestinationIndex];
1026 destination =
Connections.First().OtherLocation(
this);
1029 if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.
Locations.Count)
1031 mission.OriginLocation = map.
Locations[loadedMission.OriginLocationIndex];
1033 mission.TimesAttempted = loadedMission.TimesAttempted;
1034 availableMissions.Add(mission);
1035 if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
1037 loadedMissions =
null;
1039 if (addInitialMissionsForType !=
null)
1049 addInitialMissionsForType =
null;
1058 availableMissions.Clear();
1059 selectedMissions.Clear();
1085 return newLocationType;
1089 DebugConsole.ThrowError($
"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found.");
1097 System.Diagnostics.Debug.Assert(
Connections.Contains(connection));
1105 DebugConsole.ThrowErrorLocalized(
"Cannot hire a character from location \"" +
DisplayName +
"\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
1110 DebugConsole.ThrowErrorLocalized(
"Cannot hire a character from location \"" +
DisplayName +
"\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
1139 private void CreateRandomName(
LocationType type, Random rand, IEnumerable<Location> existingLocations)
1148 if (nameIdentifier.IsEmpty)
1152 if (rawName.IsNullOrEmpty())
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.");
1157 nameIdentifier = rawName.ToIdentifier();
1164 nameFormatIndex = rand.Next() % type.
NameFormats.Count;
1175 nameFormatIndex = rand.Next() % type.
NameFormats.Count;
1180 private static LocalizedString GetName(LocationType type,
int nameFormatIndex, Identifier nameId)
1182 if (type?.NameFormats ==
null || !type.NameFormats.Any())
1184 return TextManager.Get(nameId);
1186 return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace(
"[name]", TextManager.Get(nameId).Value);
1191 rawName =
string.Empty;
1192 nameIdentifier = nameId;
1198 UpdateStoreIdentifiers();
1200 foreach (var storeElement
in locationElement.GetChildElements(
"store"))
1202 Stores ??=
new Dictionary<Identifier, StoreInfo>();
1203 var identifier = storeElement.GetAttributeIdentifier(
"identifier",
"");
1204 if (identifier.IsEmpty)
1211 if (!
Stores.ContainsKey(identifier))
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);
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);
1245 foreach (
Item item
in items)
1247 if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.
ID)) {
continue; }
1251 DebugConsole.ThrowError(
"Tried to register a non-outpost item as being taken from the outpost.");
1263 foreach (
Character character
in characters)
1265 if (character?.Info ==
null) {
continue; }
1272 foreach (
TakenItem takenItem
in takenItems)
1281 float discount = 0.0f;
1291 float discount = 0.0f;
1296 return (
int) Math.Ceiling((1.0f - discount) * cost *
PriceMultiplier);
1301 if (
Stores !=
null &&
Stores.TryGetValue(identifier, out var store))
1313 if (!force &&
Stores !=
null) {
return; }
1314 UpdateStoreIdentifiers();
1318 foreach (var storeIdentifier
in Stores.Keys)
1322 Stores.Remove(storeIdentifier);
1327 if (
Stores.TryGetValue(identifier, out var store))
1330 var newStock = store.CreateStock();
1331 foreach (var oldStockItem
in store.Stock)
1333 if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem)
1335 if (oldStockItem.Quantity > newStockItem.Quantity)
1337 newStockItem.Quantity = oldStockItem.Quantity;
1341 store.Stock.Clear();
1342 store.Stock.AddRange(newStock);
1343 store.GenerateSpecials();
1344 store.GeneratePriceModifier();
1348 AddNewStore(identifier);
1356 AddNewStore(identifier);
1370 var storesToRemove =
new HashSet<Identifier>();
1371 foreach (var store
in Stores.Values)
1375 storesToRemove.Add(store.Identifier);
1382 var stock =
new List<PurchasedItem>(store.Stock);
1383 var stockToRemove =
new List<PurchasedItem>();
1387 var existingStock = stock.FirstOrDefault(s => s.ItemPrefab == itemPrefab);
1388 if (itemPrefab.CanBeBoughtFrom(store, out
PriceInfo priceInfo))
1390 if (existingStock ==
null)
1397 existingStock.Quantity =
1399 existingStock.Quantity + 1,
1400 priceInfo.MaxAvailableAmount);
1403 else if (existingStock !=
null)
1405 stockToRemove.Add(existingStock);
1409 stockToRemove.ForEach(i => stock.Remove(i));
1410 store.Stock.Clear();
1411 store.Stock.AddRange(stock);
1412 store.GeneratePriceModifier();
1415 StepsSinceSpecialsUpdated++;
1416 foreach (var identifier
in storesToRemove)
1418 Stores.Remove(identifier);
1422 AddNewStore(identifier);
1440 private void UpdateStoreIdentifiers()
1445 if (!outpostParam.AllowedLocationTypes.Contains(
Type.
Identifier)) {
continue; }
1446 foreach (var identifier
in outpostParam.GetStoreIdentifiers())
1453 private void AddNewStore(Identifier identifier)
1455 Stores ??=
new Dictionary<Identifier, StoreInfo>();
1456 if (
Stores.ContainsKey(identifier)) {
return; }
1457 var newStore =
new StoreInfo(
this, identifier);
1458 Stores.Add(identifier, newStore);
1461 public void AddStock(Dictionary<Identifier, List<SoldItem>> items)
1463 if (items ==
null) {
return; }
1464 foreach (var storeItems
in items)
1466 if (
GetStore(storeItems.Key) is { } store)
1468 store.AddStock(storeItems.Value);
1473 public void RemoveStock(Dictionary<Identifier, List<PurchasedItem>> items)
1475 if (items ==
null) {
return; }
1476 foreach (var storeItems
in items)
1478 if (
GetStore(storeItems.Key) is { } store)
1480 store.RemoveStock(storeItems.Value);
1488 if (!characters.Any()) {
return 0; }
1489 return characters.Max(
static c => (
int)c.GetStatValue(
StatTypes.ExtraSpecialSalesCount));
1523 PendingLocationTypeChange =
null;
1531 public XElement
Save(
Map map, XElement parentElement)
1533 var locationElement =
new XElement(
"location",
1539 new XAttribute(
"biome",
Biome?.Identifier.Value ??
string.Empty),
1540 new XAttribute(
"position", XMLExtensions.Vector2ToString(
MapPosition)),
1546 new XAttribute(
"stepssincespecialsupdated", StepsSinceSpecialsUpdated));
1548 if (!rawName.IsNullOrEmpty())
1550 locationElement.Add(
new XAttribute(nameof(rawName), rawName));
1554 locationElement.Add(
new XAttribute(nameof(nameIdentifier), nameIdentifier));
1555 locationElement.Add(
new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
1560 locationElement.Add(
new XAttribute(
"faction",
Faction.
Prefab.Identifier));
1580 if (PendingLocationTypeChange.HasValue)
1582 var changeElement =
new XElement(
"pendinglocationtypechange",
new XAttribute(
"timer", PendingLocationTypeChange.Value.delay));
1583 if (PendingLocationTypeChange.Value.parentMission !=
null)
1585 changeElement.Add(
new XAttribute(
"missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
1586 locationElement.Add(changeElement);
1590 int index =
Type.
CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange);
1591 changeElement.Add(
new XAttribute(
"index", index));
1594 DebugConsole.AddWarning($
"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
1598 locationElement.Add(changeElement);
1608 if (takenItems.Any())
1610 locationElement.Add(
new XAttribute(
1612 string.Join(
',', takenItems.Select(it => it.Identifier +
";" + it.OriginalID +
";" + it.OriginalContainerIndex +
";" + it.ModuleIndex))));
1614 if (killedCharacterIdentifiers.Any())
1616 locationElement.Add(
new XAttribute(
"killedcharacters",
string.Join(
',', killedCharacterIdentifiers)));
1621 foreach (var store
in Stores.Values)
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));
1631 storeElement.Add(
new XElement(
"stock",
1633 new XAttribute(
"qty", item.
Quantity)));
1635 if (store.DailySpecials.Any())
1637 var dailySpecialElement =
new XElement(
"dailyspecials");
1638 foreach (var item
in store.DailySpecials)
1640 dailySpecialElement.Add(
new XElement(
"item",
1641 new XAttribute(
"id", item.Identifier)));
1643 storeElement.Add(dailySpecialElement);
1645 if (store.RequestedGoods.Any())
1647 var requestedGoodsElement =
new XElement(
"requestedgoods");
1648 foreach (var item
in store.RequestedGoods)
1650 requestedGoodsElement.Add(
new XElement(
"item",
1651 new XAttribute(
"id", item.Identifier)));
1653 storeElement.Add(requestedGoodsElement);
1655 locationElement.Add(storeElement);
1661 var missionsElement =
new XElement(
"missions");
1662 foreach (
Mission mission
in missions)
1664 var location = mission.
Locations.All(l => l ==
this) ? this : mission.
Locations.FirstOrDefault(l => l !=
this);
1665 var destinationIndex = map.
Locations.IndexOf(location);
1667 missionsElement.Add(
new XElement(
"mission",
1669 new XAttribute(
"destinationindex", destinationIndex),
1671 new XAttribute(
"origin", originIndex),
1672 new XAttribute(
"selected", selectedMissions.Contains(mission))));
1674 locationElement.Add(missionsElement);
1677 parentElement.Add(locationElement);
1679 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)
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()
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
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.