2 using Microsoft.Xna.Framework;
4 using System.Collections.Generic;
17 private Location furthestDiscoveredLocation;
19 public int Width {
get;
private set; }
20 public int Height {
get;
private set; }
40 public readonly NamedEvent<LocationChangeInfo>
OnLocationChanged =
new NamedEvent<LocationChangeInfo>();
42 private List<Location> endLocations =
new List<Location>();
43 public IReadOnlyList<Location>
EndLocations {
get {
return endLocations; } }
68 public string Seed {
get;
private set; }
70 public List<Location>
Locations {
get;
private set; }
72 private readonly List<Location> locationsDiscovered =
new List<Location>();
73 private readonly List<Location> locationsVisited =
new List<Location>();
75 public List<LocationConnection>
Connections {
get;
private set; }
79 private bool trackedLocationDiscoveryAndVisitOrder =
true;
81 public Map(CampaignSettings settings)
84 Width = generationParams.Width;
85 Height = generationParams.Height;
88 if (generationParams.RadiationParams !=
null)
92 Enabled = settings.RadiationEnabled
100 private Map(
CampaignMode campaign, XElement element) : this(campaign.Settings)
102 Seed = element.GetAttributeString(
"seed",
"a");
103 Rand.SetSyncedSeed(ToolBox.StringToInt(
Seed));
105 Width = element.GetAttributeInt(
"width",
Width);
108 bool lairsFound =
false;
110 foreach (var subElement
in element.Elements())
112 switch (subElement.Name.ToString().ToLowerInvariant())
115 int i = subElement.GetAttributeInt(
"i", 0);
120 lairsFound |= subElement.GetAttributeString(
"type",
"").Equals(
"lair", StringComparison.OrdinalIgnoreCase);
121 Locations[i] =
new Location(campaign, subElement);
126 Enabled = campaign.
Settings.RadiationEnabled
132 List<XElement> connectionElements =
new List<XElement>();
133 foreach (var subElement
in element.Elements())
135 switch (subElement.Name.ToString().ToLowerInvariant())
138 Point locationIndices = subElement.GetAttributePoint(
"locations",
new Point(0, 1));
139 if (locationIndices.X == locationIndices.Y) {
continue; }
140 var connection =
new LocationConnection(
Locations[locationIndices.X],
Locations[locationIndices.Y])
142 Passed = subElement.GetAttributeBool(
"passed",
false),
143 Locked = subElement.GetAttributeBool(
"locked",
false),
144 Difficulty = subElement.GetAttributeFloat(
"difficulty", 0.0f)
146 Locations[locationIndices.X].Connections.Add(connection);
147 Locations[locationIndices.Y].Connections.Add(connection);
148 string biomeId = subElement.GetAttributeString(
"biome",
"");
150 Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ??
151 Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ??
152 Biome.Prefabs.First();
153 connection.Difficulty = MathHelper.Clamp(connection.Difficulty, connection.Biome.MinDifficulty, connection.Biome.AdjustedMaxDifficulty);
154 connection.LevelData =
new LevelData(subElement.Element(
"Level"), connection.Difficulty);
156 connectionElements.Add(subElement);
163 Random rand =
new MTRandom(ToolBox.StringToInt(
Seed));
169 int startLocationindex = element.GetAttributeInt(
"startlocation", -1);
170 if (startLocationindex >= 0 && startLocationindex <
Locations.Count)
176 DebugConsole.AddWarning($
"Error while loading the map. Start location index out of bounds (index: {startLocationindex}, location count: {Locations.Count}).");
179 if (!location.Type.HasOutpost) {
continue; }
187 if (element.GetAttribute(
"endlocation") !=
null)
190 int endLocationIndex = element.GetAttributeInt(
"endlocation", -1);
191 if (endLocationIndex >= 0 && endLocationIndex <
Locations.Count)
193 endLocations.Add(
Locations[endLocationIndex]);
197 DebugConsole.AddWarning($
"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
202 int[] endLocationindices = element.GetAttributeIntArray(
"endlocations", Array.Empty<
int>());
203 foreach (
int endLocationIndex
in endLocationindices)
205 if (endLocationIndex >= 0 && endLocationIndex <
Locations.Count)
207 endLocations.Add(
Locations[endLocationIndex]);
211 DebugConsole.AddWarning($
"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count}).");
216 if (!endLocations.Any())
218 DebugConsole.AddWarning($
"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location...");
219 Location endLocation =
null;
222 if (endLocation ==
null || location.MapPosition.X > endLocation.MapPosition.X)
224 endLocation = location;
227 endLocations.Add(endLocation);
230 System.Diagnostics.Debug.Assert(endLocations.First().Biome !=
null,
"End location biome was null.");
231 System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome,
"The biome of the end location isn't the end biome.");
238 for (
int i = 0; i < missingOutpostCount; i++)
240 Vector2 mapPos =
new Vector2(
241 MathHelper.Lerp(firstEndLocation.MapPosition.X,
Width, MathHelper.Lerp(0.2f, 0.8f, i / (
float)missingOutpostCount)),
242 Height * MathHelper.Lerp(0.2f, 1.0f, (
float)rand.NextDouble()));
243 var newEndLocation =
new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations:
Locations)
245 Biome = endLocations.First().Biome
247 newEndLocation.LevelData =
new LevelData(newEndLocation,
this, difficulty: 100.0f);
249 endLocations.Add(newEndLocation);
253 if (lairsFound && !
Connections.Any(c => c.LevelData.HasHuntingGrounds))
257 Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) <
Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability;
258 connectionElements[i].SetAttributeValue(
"hashuntinggrounds",
true);
264 if (endLocation.Type?.ForceLocationName is { IsEmpty: false })
266 endLocation.ForceName(endLocation.Type.ForceLocationName);
270 AssignEndLocationLevelData();
273 float maxX =
Locations.Select(l => l.MapPosition.X).Max();
274 if (maxX >
Width) {
Width = (int)(maxX + 10); }
275 float maxY =
Locations.Select(l => l.MapPosition.Y).Max();
278 InitProjectSpecific();
287 Rand.SetSyncedSeed(ToolBox.StringToInt(
Seed));
293 throw new Exception($
"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}");
296 FindStartLocation(l => l.Type.Identifier ==
"outpost");
300 FindStartLocation(l => l.Type.HasOutpost && l.Type.OutpostTeam ==
CharacterTeamType.FriendlyNPC);
303 void FindStartLocation(Func<Location, bool> predicate)
307 if (!predicate(location)) {
continue; }
316 var startOutpostFaction = campaign?.
Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
317 if (startOutpostFaction !=
null)
327 if (!otherLocation.HasOutpost())
331 otherLocation.ChangeType(campaign, outpostLocationType);
335 if (otherLocation.HasOutpost() &&
337 otherLocation.Type.Faction.IsEmpty)
339 otherLocation.
Faction = startOutpostFaction;
343 System.Diagnostics.Debug.Assert(
StartLocation !=
null,
"Start location not assigned after level generation.");
345 int loops = campaign.
CampaignMetadata.GetInt(
"campaign.endings".ToIdentifier(), 0);
356 if (locationConnection.Difficulty > 0.0f)
358 locationConnection.Difficulty = 0.0f;
359 locationConnection.LevelData =
new LevelData(locationConnection);
374 location.UnlockInitialMissions();
377 InitProjectSpecific();
380 partial
void InitProjectSpecific();
389 List<Vector2> voronoiSites =
new List<Vector2>();
390 for (
float x = 10.0f; x <
Width - 10.0f; x += generationParams.VoronoiSiteInterval.X)
392 for (
float y = 10.0f; y <
Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y)
394 voronoiSites.Add(
new Vector2(
395 x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient),
396 y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
403 Vector2 margin =
new Vector2(
404 Math.Min(10,
Width * 0.1f),
405 Math.Min(10,
Height * 0.2f));
407 float startX = margin.X, endX =
Width - margin.X;
408 float startY = margin.Y, endY =
Height - margin.Y;
412 throw new Exception($
"Generating a campaign map failed (no edges in the voronoi graph). Width: {Width}, height: {Height}, margin: {margin}");
415 voronoiSites.Clear();
416 Dictionary<int, List<Location>> locationsPerZone =
new Dictionary<int, List<Location>>();
417 bool possibleStartOutpostCreated =
false;
420 if (edge.
Point1 == edge.Point2) {
continue; }
426 if (edge.Point2.X < margin.X || edge.Point2.X >
Width - margin.X || edge.Point2.Y < startY || edge.Point2.Y > endY)
431 Location[] newLocations =
new Location[2];
432 newLocations[0] =
Locations.Find(l => l.MapPosition == edge.
Point1 || l.MapPosition == edge.Point2);
433 newLocations[1] =
Locations.Find(l => l != newLocations[0] && (l.MapPosition == edge.
Point1 || l.MapPosition == edge.Point2));
435 for (
int i = 0; i < 2; i++)
437 if (newLocations[i] !=
null) {
continue; }
439 Vector2[] points =
new Vector2[] { edge.
Point1, edge.Point2 };
441 int positionIndex = Rand.Int(1, Rand.RandSync.ServerAndClient);
443 Vector2 position = points[positionIndex];
444 if (newLocations[1 - i] !=
null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
445 int zone = GetZoneIndex(position.X);
446 if (!locationsPerZone.ContainsKey(zone))
448 locationsPerZone[zone] =
new List<Location>();
451 LocationType forceLocationType =
null;
452 if (!possibleStartOutpostCreated)
454 float zoneWidth =
Width / generationParams.DifficultyZones;
455 float threshold = zoneWidth * 0.1f;
456 if (position.X < threshold)
458 LocationType.Prefabs.TryGet(
"outpost", out forceLocationType);
459 possibleStartOutpostCreated =
true;
463 if (forceLocationType ==
null)
465 foreach (LocationType locationType
in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
467 if (locationType.MinCountPerZone.TryGetValue(zone, out
int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
469 forceLocationType = locationType;
475 newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
476 requireOutpost:
false, forceLocationType: forceLocationType, existingLocations:
Locations);
477 locationsPerZone[zone].Add(newLocations[i]);
481 var newConnection =
new LocationConnection(newLocations[0], newLocations[1]);
486 float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
491 if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
499 foreach (LocationConnection connection2
in Connections)
501 if (connection2.Locations[0] == connection.Locations[0]) { connection2.Locations[0] = connection.Locations[1]; }
502 if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; }
506 foreach (LocationConnection connection
in Connections)
508 connection.Locations[0].Connections.Add(connection);
509 connection.Locations[1].Connections.Add(connection);
513 float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
514 for (
int i =
Locations.Count - 1; i >= 0; i--)
516 for (
int j =
Locations.Count - 1; j > i; j--)
518 float dist = Vector2.DistanceSquared(
Locations[i].MapPosition,
Locations[j].MapPosition);
519 if (dist > minLocationDistanceSqr)
526 if (connection.Locations[0] ==
Locations[j])
535 if (connection.Locations[0] != connection.Locations[1])
537 Locations[i].Connections.Add(connection);
560 for (
int n = Math.Min(i - 1,
Connections.Count - 1); n >= 0; n--)
562 if (connection.Locations.Contains(
Connections[n].Locations[0])
563 && connection.Locations.Contains(
Connections[n].Locations[1]))
570 List<LocationConnection>[] connectionsBetweenZones =
new List<LocationConnection>[generationParams.DifficultyZones];
571 for (
int i = 0; i < generationParams.DifficultyZones; i++)
573 connectionsBetweenZones[i] =
new List<LocationConnection>();
576 shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient);
577 foreach (var connection
in shuffledConnections)
579 int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
580 int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
581 if (zone1 == zone2) {
continue; }
584 (zone1, zone2) = (zone2, zone1);
587 if (generationParams.GateCount[zone1] == 0) {
continue; }
589 if (!connectionsBetweenZones[zone1].
Any())
591 connectionsBetweenZones[zone1].Add(connection);
593 else if (generationParams.GateCount[zone1] == 1)
596 if (Math.Abs(connection.CenterPos.Y -
Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y -
Height / 2))
598 connectionsBetweenZones[zone1].Clear();
599 connectionsBetweenZones[zone1].Add(connection);
602 else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
603 connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
605 connectionsBetweenZones[zone1].Add(connection);
609 var gateFactions = campaign.
Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
614 if (zone1 == zone2) {
continue; }
615 if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) {
continue; }
617 int leftZone = Math.Min(zone1, zone2);
618 if (generationParams.GateCount[leftZone] == 0) {
continue; }
619 if (!connectionsBetweenZones[leftZone].Contains(
Connections[i]))
625 var leftMostLocation =
629 if (!AllowAsBiomeGate(leftMostLocation.Type))
631 leftMostLocation.ChangeType(
633 LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
634 createStores:
false);
636 static bool AllowAsBiomeGate(LocationType lt)
640 return lt.HasOutpost && lt.Identifier !=
"abandoned" && lt.AllowAsBiomeGate;
643 leftMostLocation.IsGateBetweenBiomes =
true;
646 if (leftMostLocation.Type.HasOutpost && campaign !=
null && gateFactions.Any())
648 leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(
Connections[i]) % gateFactions.Count];
655 for (
int i = location.Connections.Count - 1; i >= 0; i--)
657 if (!
Connections.Contains(location.Connections[i]))
659 location.Connections.RemoveAt(i);
670 if (!connection.Locked) {
continue; }
671 var rightMostLocation =
672 connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
673 connection.Locations[0] :
674 connection.Locations[1];
677 if (rightMostLocation.Connections.Count == 1)
679 Location closestLocation =
null;
680 float closestDist =
float.PositiveInfinity;
681 foreach (Location otherLocation
in Locations)
683 if (otherLocation == rightMostLocation || otherLocation.MapPosition.X < rightMostLocation.MapPosition.X) {
continue; }
684 float dist = Vector2.DistanceSquared(rightMostLocation.MapPosition, otherLocation.MapPosition);
685 if (dist < closestDist || closestLocation ==
null)
687 closestLocation = otherLocation;
692 var newConnection =
new LocationConnection(rightMostLocation, closestLocation);
693 rightMostLocation.Connections.Add(newConnection);
694 closestLocation.Connections.Add(newConnection);
696 GenerateLocationConnectionVisuals(newConnection);
703 AssignBiomes(
new MTRandom(ToolBox.StringToInt(
Seed)));
705 foreach (LocationConnection connection
in Connections)
707 if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
709 connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty);
713 connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
719 location.LevelData =
new LevelData(location,
this, CalculateDifficulty(location.MapPosition.X, location.Biome));
720 location.TryAssignFactionBasedOnLocationType(campaign);
721 if (location.Type.HasOutpost && campaign !=
null && location.Type.OutpostTeam ==
CharacterTeamType.FriendlyNPC)
723 if (location.Type.Faction.IsEmpty)
726 location.Faction ??= campaign.
GetRandomFaction(Rand.RandSync.ServerAndClient);
728 if (location.Type.SecondaryFaction.IsEmpty)
734 location.CreateStores(force:
true);
737 foreach (LocationConnection connection
in Connections)
739 connection.LevelData =
new LevelData(connection);
742 CreateEndLocation(campaign);
744 float CalculateDifficulty(
float mapPosition, Biome biome)
746 float settingsFactor = campaign.
Settings.LevelDifficultyMultiplier;
747 float minDifficulty = 0;
748 float maxDifficulty = 100;
749 float difficulty = mapPosition /
Width * 100;
750 System.Diagnostics.Debug.Assert(biome !=
null);
753 minDifficulty = biome.MinDifficulty;
754 maxDifficulty = biome.AdjustedMaxDifficulty;
755 float diff = 1 - settingsFactor;
756 difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
758 return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
762 partial
void GenerateAllLocationConnectionVisuals();
764 partial
void GenerateLocationConnectionVisuals(LocationConnection connection);
766 private int GetZoneIndex(
float xPos)
768 float zoneWidth =
Width / generationParams.DifficultyZones;
769 return MathHelper.Clamp((
int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
779 float zoneWidth =
Width / generationParams.DifficultyZones;
780 int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
781 zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
782 return Biome.
Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
785 private void AssignBiomes(Random rand)
788 float zoneWidth =
Width / generationParams.DifficultyZones;
790 List<Biome> allowedBiomes =
new List<Biome>(10);
791 for (
int i = 0; i < generationParams.DifficultyZones; i++)
793 allowedBiomes.Clear();
794 allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
795 float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
801 location.
Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
805 foreach (LocationConnection connection
in Connections)
807 if (connection.Biome !=
null) {
continue; }
808 connection.
Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome;
811 System.Diagnostics.Debug.Assert(
Locations.All(l => l.Biome !=
null));
812 System.Diagnostics.Debug.Assert(
Connections.All(c => c.Biome !=
null));
815 private Location GetPreviousToEndLocation()
817 Location previousToEndLocation =
null;
820 if (!location.Biome.IsEndBiome && (previousToEndLocation ==
null || location.MapPosition.X > previousToEndLocation.MapPosition.X))
822 previousToEndLocation = location;
825 return previousToEndLocation;
828 private void ForceLocationTypeToNone(CampaignMode campaign, Location location)
830 if (LocationType.Prefabs.TryGet(
"none", out LocationType locationType))
832 location.ChangeType(campaign, locationType, createStores:
false);
834 location.DisallowLocationTypeChanges =
true;
837 private void CreateEndLocation(CampaignMode campaign)
839 float zoneWidth =
Width / generationParams.DifficultyZones;
840 Vector2 endPos =
new Vector2(
Width - zoneWidth * 0.7f,
Height / 2);
841 float closestDist =
float.MaxValue;
845 float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
846 if (location.Biome.IsEndBiome && dist < closestDist)
848 endLocation = location;
853 var previousToEndLocation = GetPreviousToEndLocation();
854 if (endLocation ==
null || previousToEndLocation ==
null) {
return; }
856 endLocations =
new List<Location>() { endLocation };
857 if (endLocation.Biome.EndBiomeLocationCount > 1)
859 FindConnectedEndLocations(endLocation);
861 void FindConnectedEndLocations(Location currLocation)
863 if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) {
return; }
864 foreach (var connection
in currLocation.Connections)
866 if (connection.Biome != endLocation.Biome) {
continue; }
867 var otherLocation = connection.OtherLocation(currLocation);
868 if (otherLocation !=
null && !endLocations.Contains(otherLocation))
870 if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) {
return; }
871 endLocations.Add(otherLocation);
872 FindConnectedEndLocations(otherLocation);
878 ForceLocationTypeToNone(campaign, previousToEndLocation);
881 for (
int i =
Locations.Count - 1; i >= 0; i--)
888 var connection =
Locations[i].Connections[j];
889 var otherLocation = connection.OtherLocation(
Locations[i]);
891 otherLocation?.Connections.Remove(connection);
894 if (!endLocations.Contains(
Locations[i]))
902 if (previousToEndLocation.Connections.None())
907 if (!location.Biome.IsEndBiome && location != previousToEndLocation && location.MapPosition.X > connectTo.MapPosition.X)
909 connectTo = location;
912 var newConnection =
new LocationConnection(previousToEndLocation, connectTo)
914 Biome = endLocation.Biome,
917 newConnection.LevelData =
new LevelData(newConnection);
919 previousToEndLocation.Connections.Add(newConnection);
920 connectTo.Connections.Add(newConnection);
923 var endConnection =
new LocationConnection(previousToEndLocation, endLocation)
925 Biome = endLocation.Biome,
928 endConnection.LevelData =
new LevelData(endConnection);
930 previousToEndLocation.Connections.Add(endConnection);
931 endLocation.Connections.Add(endConnection);
933 AssignEndLocationLevelData();
936 private void AssignEndLocationLevelData()
938 for (
int i = 0; i < endLocations.Count; i++)
940 endLocations[i].LevelData.ReassignGenerationParams(
Seed);
941 var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
942 if (outpostParams !=
null)
944 endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams;
949 private void ExpandBiomes(List<LocationConnection> seeds)
951 List<LocationConnection> nextSeeds =
new List<LocationConnection>();
952 foreach (LocationConnection connection
in seeds)
954 foreach (Location location
in connection.Locations)
956 foreach (LocationConnection otherConnection
in location.Connections)
958 if (otherConnection == connection)
continue;
959 if (otherConnection.Biome !=
null)
continue;
961 otherConnection.Biome = connection.Biome;
962 nextSeeds.Add(otherConnection);
967 if (nextSeeds.Count > 0)
969 ExpandBiomes(nextSeeds);
974 #endregion Generation
988 DebugConsole.ThrowError(
"Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
997 if (currentEndLocationIndex < endLocations.Count - 1)
1010 DebugConsole.ThrowError(
"Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
1042 CurrentLocation =
null;
1046 if (index < 0 || index >= Locations.Count)
1048 DebugConsole.ThrowError(
"Location index out of bounds");
1052 Location prevLocation = CurrentLocation;
1053 CurrentLocation = Locations[index];
1054 Discover(CurrentLocation);
1056 CurrentLocation.CreateStores();
1057 if (prevLocation != CurrentLocation)
1059 var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
1060 if (connection !=
null)
1062 connection.Passed =
true;
1072 SelectedLocation =
null;
1073 SelectedConnection =
null;
1075 OnLocationSelected?.Invoke(
null,
null);
1079 if (index < 0 || index >= Locations.Count)
1081 DebugConsole.ThrowError(
"Location index out of bounds");
1085 Location prevSelected = SelectedLocation;
1086 SelectedLocation = Locations[index];
1088 if (currentDisplayLocation == SelectedLocation)
1090 SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1094 SelectedConnection =
1095 Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
1096 Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1098 if (SelectedConnection?.Locked ??
false)
1101 $
"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
1102 $
" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
1103 + Environment.StackTrace.CleanupStackTrace();
1104 GameAnalyticsManager.AddErrorEventOnce(
"MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1105 DebugConsole.ThrowError(errorMsg);
1107 if (prevSelected != SelectedLocation)
1109 OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1115 if (!Locations.Contains(location))
1117 string errorMsg = $
"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
1118 DebugConsole.ThrowError(errorMsg);
1119 GameAnalyticsManager.AddErrorEventOnce(
"Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1123 Location prevSelected = SelectedLocation;
1124 SelectedLocation = location;
1125 SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1126 if (SelectedConnection?.Locked ??
false)
1128 DebugConsole.ThrowError(
"A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
1130 if (prevSelected != SelectedLocation)
1132 OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1138 if (CurrentLocation ==
null)
1140 string errorMsg =
"Failed to select a mission (current location not set).";
1141 DebugConsole.ThrowError(errorMsg);
1142 GameAnalyticsManager.AddErrorEventOnce(
"Map.SelectMission:CurrentLocationNotSet", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1146 if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
1148 CurrentLocation.SetSelectedMissionIndices(missionIndices);
1149 foreach (
Mission selectedMission
in CurrentLocation.SelectedMissions.ToList())
1151 if (selectedMission.
Locations[0] != CurrentLocation ||
1152 selectedMission.
Locations[1] != CurrentLocation)
1154 if (SelectedConnection ==
null) {
return; }
1156 if (selectedMission.
Locations[1] != SelectedLocation)
1158 CurrentLocation.DeselectMission(selectedMission);
1162 OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
1168 List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
1169 List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
1171 if (undiscoveredLocations.Count > 0 && preferUndiscovered)
1173 SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
1177 SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
1184 int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f));
1189 steps = Math.Max(1, steps);
1191 steps = Math.Min(steps, 5);
1192 for (
int i = 0; i < steps; i++)
1194 ProgressWorld(campaign);
1198 for (
int i = 0; i < Math.Max(1, steps); i++)
1200 foreach (
Location location
in Locations)
1207 Radiation?.OnStep(steps);
1212 foreach (
Location location
in Locations)
1217 if (furthestDiscoveredLocation ==
null ||
1220 furthestDiscoveredLocation = location;
1224 foreach (LocationConnection connection
in Connections)
1229 foreach (Location location
in Locations)
1231 if (location.MapPosition.X > furthestDiscoveredLocation.
MapPosition.X)
1236 if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) {
continue; }
1238 if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
1240 location.UpdateStores();
1245 private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location)
1247 location.TimeSinceLastTypeChange++;
1248 location.LocationTypeChangeCooldown--;
1250 if (location.PendingLocationTypeChange !=
null)
1252 if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
1255 location.PendingLocationTypeChange =
null;
1259 location.PendingLocationTypeChange =
1260 (location.PendingLocationTypeChange.Value.typeChange,
1261 location.PendingLocationTypeChange.Value.delay - 1,
1262 location.PendingLocationTypeChange.Value.parentMission);
1263 if (location.PendingLocationTypeChange.Value.delay <= 0)
1265 return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
1271 Dictionary<LocationTypeChange, float> allowedTypeChanges =
new Dictionary<LocationTypeChange, float>();
1272 foreach (LocationTypeChange typeChange
in location.Type.CanChangeTo)
1274 float probability = typeChange.DetermineProbability(location);
1275 if (probability <= 0.0f) {
continue; }
1276 allowedTypeChanges.Add(typeChange, probability);
1280 if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
1282 var selectedTypeChange =
1283 ToolBox.SelectWeightedRandom(
1284 allowedTypeChanges.Keys.ToList(),
1285 allowedTypeChanges.Values.ToList(),
1286 Rand.RandSync.Unsynced);
1287 if (selectedTypeChange !=
null)
1289 if (selectedTypeChange.RequiredDurationRange.X > 0)
1291 location.PendingLocationTypeChange =
1292 (selectedTypeChange,
1293 Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
1298 return ChangeLocationType(campaign, location, selectedTypeChange);
1304 foreach (LocationTypeChange typeChange
in location.Type.CanChangeTo)
1306 foreach (var requirement
in typeChange.Requirements)
1308 if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
1310 if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
1311 location.ProximityTimer[requirement] += 1;
1315 location.ProximityTimer.Remove(requirement);
1323 private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
1325 LocalizedString prevName = location.DisplayName;
1327 if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
1329 DebugConsole.ThrowError($
"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
1333 if (location.LocationTypeChangesBlocked) {
return false; }
1335 if (newType.OutpostTeam != location.Type.OutpostTeam ||
1336 newType.HasOutpost != location.Type.HasOutpost)
1338 location.ClearMissions();
1340 location.ChangeType(campaign, newType);
1341 ChangeLocationTypeProjSpecific(location, prevName, change);
1342 foreach (var requirement
in change.Requirements)
1344 location.ProximityTimer.Remove(requirement);
1346 location.TimeSinceLastTypeChange = 0;
1347 location.LocationTypeChangeCooldown = change.CooldownAfterChange;
1348 location.PendingLocationTypeChange =
null;
1354 return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
1364 var locationsToTest =
new List<Location>() { startLocation };
1365 var nextBatchToTest =
new HashSet<Location>();
1366 var checkedLocations =
new HashSet<Location>();
1367 while (locationsToTest.Any())
1369 foreach (var location
in locationsToTest)
1371 checkedLocations.Add(location);
1372 if (criteria(location)) {
return distance; }
1373 foreach (var connection
in location.Connections)
1375 if (connectionCriteria !=
null && connectionCriteria(connection))
1379 var otherLocation = connection.OtherLocation(location);
1380 if (!checkedLocations.Contains(otherLocation))
1382 nextBatchToTest.Add(otherLocation);
1385 if (distance > maxDistance) {
return int.MaxValue; }
1388 locationsToTest.Clear();
1389 locationsToTest.AddRange(nextBatchToTest);
1390 nextBatchToTest.Clear();
1392 return int.MaxValue;
1398 partial
void ClearAnimQueue();
1402 if (location is
null) {
return; }
1403 if (locationsDiscovered.Contains(location)) {
return; }
1404 locationsDiscovered.Add(location);
1413 if (location is
null) {
return; }
1414 if (locationsVisited.Contains(location)) {
return; }
1415 locationsVisited.Add(location);
1416 RemoveFogOfWarProjSpecific(location);
1421 locationsDiscovered.Clear();
1422 locationsVisited.Clear();
1427 if (!trackedLocationDiscoveryAndVisitOrder) {
return null; }
1428 if (location is
null) {
return -1; }
1429 return locationsDiscovered.IndexOf(location);
1434 if (!trackedLocationDiscoveryAndVisitOrder) {
return null; }
1435 if (location is
null) {
return -1; }
1436 int index = locationsVisited.IndexOf(location);
1437 if (includeLocationsWithoutOutpost) {
return index; }
1438 int noOutpostLocations = 0;
1439 for (
int i = 0; i < index; i++)
1441 if (locationsVisited[i] is not
Location l) {
continue; }
1442 if (l.HasOutpost()) {
continue; }
1443 noOutpostLocations++;
1445 return index - noOutpostLocations;
1450 if (location is
null) {
return false; }
1451 return locationsDiscovered.Contains(location);
1456 if (location is
null) {
return false; }
1457 return locationsVisited.Contains(location);
1460 partial
void RemoveFogOfWarProjSpecific(
Location location);
1467 Map map =
new Map(campaign, element);
1468 map.
LoadState(campaign, element,
false);
1481 SetLocation(element.GetAttributeInt(
"currentlocation", 0));
1483 if (!Version.TryParse(element.GetAttributeString(
"version",
""), out Version version))
1485 DebugConsole.ThrowError(
"Incompatible map save file, loading the game failed.");
1489 ClearLocationHistory();
1490 foreach (var subElement
in element.Elements())
1492 switch (subElement.Name.ToString().ToLowerInvariant())
1495 int locationIndex = subElement.GetAttributeInt(
"i", -1);
1496 if (locationIndex < 0 || locationIndex >= Locations.Count)
1498 DebugConsole.AddWarning($
"Error while loading the campaign map: location index out of bounds ({locationIndex})");
1501 Location location = Locations[locationIndex];
1505 for (
int j = 0; j < location.
Type.
CanChangeTo[i].Requirements.Count; j++)
1514 if (subElement.GetAttributeBool(
"discovered",
false))
1518 trackedLocationDiscoveryAndVisitOrder =
false;
1521 Identifier locationType = subElement.GetAttributeIdentifier(
"type", Identifier.Empty);
1525 location.
ChangeType(campaign, newLocationType);
1527 var factionIdentifier = subElement.GetAttributeIdentifier(
"faction", Identifier.Empty);
1528 location.
Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1530 if (showNotifications && prevLocationType != location.
Type)
1535 ChangeLocationTypeProjSpecific(location, prevLocationName, change);
1540 var secondaryFactionIdentifier = subElement.GetAttributeIdentifier(
"secondaryfaction", Identifier.Empty);
1541 location.
SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
1549 if (subElement.Attribute(
"i") ==
null) {
continue; }
1551 int connectionIndex = subElement.GetAttributeInt(
"i", -1);
1552 if (connectionIndex < 0 || connectionIndex >= Connections.Count)
1554 DebugConsole.AddWarning($
"Error while loading the campaign map: connection index out of bounds ({connectionIndex})");
1557 Connections[connectionIndex].Passed = subElement.GetAttributeBool(
"passed",
false);
1558 Connections[connectionIndex].Locked = subElement.GetAttributeBool(
"locked",
false);
1561 Radiation =
new Radiation(
this, generationParams.RadiationParams, subElement);
1564 bool trackedVisitedEmptyLocations = subElement.GetAttributeBool(
"trackedvisitedemptylocations",
false);
1565 foreach (var childElement
in subElement.GetChildElements(
"location"))
1567 if (GetLocation(childElement) is
Location l)
1570 if (!trackedVisitedEmptyLocations)
1572 if (!l.HasOutpost())
1576 trackedLocationDiscoveryAndVisitOrder =
false;
1582 foreach (var childElement
in subElement.GetChildElements(
"location"))
1584 if (GetLocation(childElement) is
Location l)
1592 Location GetLocation(XElement element)
1594 int index = element.GetAttributeInt(
"i", -1);
1595 if (index < 0) {
return null; }
1596 return Locations[index];
1603 this.Discover(location, checkTalents:
false);
1604 if (furthestDiscoveredLocation ==
null || location.
MapPosition.X > furthestDiscoveredLocation.
MapPosition.X)
1606 furthestDiscoveredLocation = location;
1610 foreach (
Location location
in Locations)
1617 if (version <
new Version(1, 0) && Locations.None(l => l.Faction !=
null || l.SecondaryFaction !=
null))
1619 Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
1620 foreach (
Location location
in Locations)
1624 location.
Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
1625 if (location != StartLocation)
1627 location.
SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
1633 int currentLocationConnection = element.GetAttributeInt(
"currentlocationconnection", -1);
1634 if (currentLocationConnection >= 0)
1636 Connections[currentLocationConnection].Locked =
false;
1637 SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
1642 if (CurrentLocation !=
null && !CurrentLocation.Type.HasOutpost && SelectedConnection ==
null)
1644 DebugConsole.AddWarning($
"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
1645 SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
1649 var previousToEndLocation = GetPreviousToEndLocation();
1650 if (previousToEndLocation !=
null)
1652 ForceLocationTypeToNone(campaign, previousToEndLocation);
1658 XElement mapElement =
new XElement(
"map");
1660 mapElement.Add(
new XAttribute(
"version",
GameMain.
Version.ToString()));
1661 mapElement.Add(
new XAttribute(
"currentlocation", CurrentLocationIndex));
1664 if (campaign.NextLevel !=
null && campaign.NextLevel.Type ==
LevelData.
LevelType.LocationConnection)
1666 mapElement.Add(
new XAttribute(
"currentlocationconnection", Connections.IndexOf(CurrentLocation.Connections.Find(c => c.LevelData == campaign.NextLevel))));
1670 mapElement.Add(
new XAttribute(
"currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData ==
Level.
Loaded.
LevelData))));
1673 mapElement.Add(
new XAttribute(
"width", Width));
1674 mapElement.Add(
new XAttribute(
"height", Height));
1675 mapElement.Add(
new XAttribute(
"selectedlocation", SelectedLocationIndex));
1676 mapElement.Add(
new XAttribute(
"startlocation", Locations.IndexOf(StartLocation)));
1677 mapElement.Add(
new XAttribute(
"endlocations",
string.Join(
',', EndLocations.Select(e => Locations.IndexOf(e)))));
1678 mapElement.Add(
new XAttribute(
"seed", Seed));
1680 for (
int i = 0; i < Locations.Count; i++)
1682 var location = Locations[i];
1683 var locationElement = location.
Save(
this, mapElement);
1684 locationElement.Add(
new XAttribute(
"i", i));
1687 for (
int i = 0; i < Connections.Count; i++)
1689 var connection = Connections[i];
1691 var connectionElement =
new XElement(
"connection",
1692 new XAttribute(
"passed", connection.Passed),
1693 new XAttribute(
"locked", connection.Locked),
1694 new XAttribute(
"difficulty", connection.Difficulty),
1695 new XAttribute(
"biome", connection.Biome.Identifier),
1696 new XAttribute(
"i", i),
1697 new XAttribute(
"locations", Locations.IndexOf(connection.Locations[0]) +
"," + Locations.IndexOf(connection.Locations[1])));
1698 connection.LevelData.Save(connectionElement);
1699 mapElement.Add(connectionElement);
1702 if (Radiation !=
null)
1704 mapElement.Add(Radiation.Save());
1707 if (locationsDiscovered.Any())
1709 var discoveryElement =
new XElement(
"discovered",
1710 new XAttribute(
"trackedvisitedemptylocations",
true));
1711 foreach (
Location location
in locationsDiscovered)
1713 int index = Locations.IndexOf(location);
1714 var locationElement =
new XElement(
"location",
new XAttribute(
"i", index));
1715 discoveryElement.Add(locationElement);
1717 mapElement.Add(discoveryElement);
1720 if (locationsVisited.Any())
1722 var visitElement =
new XElement(
"visited");
1723 foreach (
Location location
in locationsVisited)
1725 int index = Locations.IndexOf(location);
1726 var locationElement =
new XElement(
"location",
new XAttribute(
"i", index));
1727 visitElement.Add(locationElement);
1729 mapElement.Add(visitElement);
1732 element.Add(mapElement);
1737 foreach (
Location location
in Locations)
1741 RemoveProjSpecific();
1744 partial
void RemoveProjSpecific();
static readonly PrefabCollection< Biome > Prefabs
Biome(ContentXElement element, LevelGenerationParametersFile file)
readonly int EndBiomeLocationCount
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
readonly CampaignMetadata CampaignMetadata
CampaignSettings Settings
IReadOnlyList< Faction > Factions
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
Location GetCurrentDisplayLocation()
The location that's displayed as the "current one" in the map screen. Normally the current outpost or...
Faction(CampaignMetadata? metadata, FactionPrefab prefab)
static GameSession?? GameSession
static readonly Version Version
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
bool EventsExhausted
'Exhaustible' sets won't appear in the same level until after one world step (~10 min,...
readonly LevelData LevelData
void ChangeType(CampaignMode campaign, LocationType newType, bool createStores=true)
void LoadStores(XElement locationElement)
int TimeSinceLastTypeChange
readonly List< LocationConnection > Connections
XElement Save(Map map, XElement parentElement)
void LoadLocationTypeChange(XElement locationElement)
Identifier NameIdentifier
List< int > GetSelectedMissionIndices()
readonly Dictionary< LocationTypeChange.Requirement, int > ProximityTimer
void InstantiateLoadedMissions(Map map)
LocalizedString DisplayName
void CreateStores(bool force=false)
If true, the stores will be recreated if they already exists.
void LoadMissions(XElement locationElement)
readonly CharacterTeamType OutpostTeam
readonly List< LocationTypeChange > CanChangeTo
static readonly PrefabCollection< LocationType > Prefabs
static MapGenerationParams Instance
void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration)
void LoadState(CampaignMode campaign, XElement element, bool showNotifications)
Load the state of an existing map from xml (current state of locations, where the crew is now,...
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
Action< Location, LocationConnection > OnLocationSelected
Map(CampaignSettings settings)
IReadOnlyList< Location > EndLocations
int? GetDiscoveryIndex(Location location)
void Visit(Location location)
IEnumerable< int > GetSelectedMissionIndices()
static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
Get the shortest distance from the start location to another location that satisfies the specified cr...
bool IsDiscovered(Location location)
static Map Load(CampaignMode campaign, XElement element)
Load a previously saved map from an xml element
void SelectMission(IEnumerable< int > missionIndices)
void SelectLocation(Location location)
List< Location > Locations
int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost=false)
int SelectedLocationIndex
static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
void SetLocation(int index)
Action< LocationConnection, IEnumerable< Mission > > OnMissionsSelected
void Discover(Location location, bool checkTalents=true)
void ClearLocationHistory()
Location SelectedLocation
void Save(XElement element)
Map(CampaignMode campaign, string seed)
Generate a new campaign map from the seed
Biome GetBiome(Vector2 mapPos)
List< LocationConnection > Connections
LocationConnection SelectedConnection
void MoveToNextLocation()
void SelectRandomLocation(bool preferUndiscovered)
bool IsVisited(Location location)
void SelectLocation(int index)
Biome GetBiome(float xPos)
readonly Location[] Locations
readonly Identifier Identifier
List< GraphEdge > MakeVoronoiGraph(List< Vector2 > sites, int width, int height)
LocationChangeInfo(Location prevLocation, Location newLocation)
readonly Location PrevLocation
readonly Location NewLocation