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]; }
446 if (!locationsPerZone.ContainsKey(zone))
448 locationsPerZone[zone] =
new List<Location>();
451 LocationType forceLocationType =
null;
452 if (forceLocationType ==
null)
454 foreach (LocationType locationType
in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
456 if (locationType.MinCountPerZone.TryGetValue(zone, out
int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
458 forceLocationType = locationType;
464 newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
465 requireOutpost:
false, forceLocationType: forceLocationType, existingLocations:
Locations);
466 locationsPerZone[zone].Add(newLocations[i]);
470 var newConnection =
new LocationConnection(newLocations[0], newLocations[1]);
475 float minConnectionDistanceSqr = generationParams.MinConnectionDistance * generationParams.MinConnectionDistance;
480 if (Vector2.DistanceSquared(connection.Locations[0].MapPosition, connection.Locations[1].MapPosition) > minConnectionDistanceSqr)
488 foreach (LocationConnection connection2
in Connections)
490 if (connection2.Locations[0] == connection.Locations[0]) { connection2.Locations[0] = connection.Locations[1]; }
491 if (connection2.Locations[1] == connection.Locations[0]) { connection2.Locations[1] = connection.Locations[1]; }
495 foreach (LocationConnection connection
in Connections)
497 connection.Locations[0].Connections.Add(connection);
498 connection.Locations[1].Connections.Add(connection);
502 float minLocationDistanceSqr = generationParams.MinLocationDistance * generationParams.MinLocationDistance;
503 for (
int i =
Locations.Count - 1; i >= 0; i--)
505 for (
int j =
Locations.Count - 1; j > i; j--)
507 float dist = Vector2.DistanceSquared(
Locations[i].MapPosition,
Locations[j].MapPosition);
508 if (dist > minLocationDistanceSqr)
515 if (connection.Locations[0] ==
Locations[j])
524 if (connection.Locations[0] != connection.Locations[1])
526 Locations[i].Connections.Add(connection);
549 for (
int n = Math.Min(i - 1,
Connections.Count - 1); n >= 0; n--)
551 if (connection.Locations.Contains(
Connections[n].Locations[0])
552 && connection.Locations.Contains(
Connections[n].Locations[1]))
559 List<LocationConnection>[] connectionsBetweenZones =
new List<LocationConnection>[generationParams.DifficultyZones];
560 for (
int i = 0; i < generationParams.DifficultyZones; i++)
562 connectionsBetweenZones[i] =
new List<LocationConnection>();
565 shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient);
566 foreach (var connection
in shuffledConnections)
568 int zone1 =
GetZoneIndex(connection.Locations[0].MapPosition.X);
569 int zone2 =
GetZoneIndex(connection.Locations[1].MapPosition.X);
570 if (zone1 == zone2) {
continue; }
573 (zone1, zone2) = (zone2, zone1);
576 if (generationParams.GateCount[zone1] == 0) {
continue; }
578 if (!connectionsBetweenZones[zone1].
Any())
580 connectionsBetweenZones[zone1].Add(connection);
582 else if (generationParams.GateCount[zone1] == 1)
585 if (Math.Abs(connection.CenterPos.Y -
Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y -
Height / 2))
587 connectionsBetweenZones[zone1].Clear();
588 connectionsBetweenZones[zone1].Add(connection);
591 else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
592 connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
594 connectionsBetweenZones[zone1].Add(connection);
596 if (connectionsBetweenZones[zone1].
None())
598 DebugConsole.ThrowError($
"Potential error during map generation: no connections between zones {zone1} and {zone2} found. Traversing through to the end of the map may be impossible.");
602 var gateFactions = campaign.
Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
607 if (zone1 == zone2) {
continue; }
608 if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) {
continue; }
610 int leftZone = Math.Min(zone1, zone2);
611 if (generationParams.GateCount[leftZone] == 0) {
continue; }
612 if (!connectionsBetweenZones[leftZone].Contains(
Connections[i]))
618 var leftMostLocation =
622 if (!AllowAsBiomeGate(leftMostLocation.Type))
624 leftMostLocation.ChangeType(
626 LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
627 createStores:
false);
629 static bool AllowAsBiomeGate(LocationType lt)
633 return lt.HasOutpost && lt.Identifier !=
"abandoned" && lt.AllowAsBiomeGate;
636 leftMostLocation.IsGateBetweenBiomes =
true;
639 if (leftMostLocation.Type.HasOutpost && campaign !=
null && gateFactions.Any())
641 leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(
Connections[i]) % gateFactions.Count];
648 for (
int i = location.Connections.Count - 1; i >= 0; i--)
650 if (!
Connections.Contains(location.Connections[i]))
652 location.Connections.RemoveAt(i);
663 if (!connection.Locked) {
continue; }
664 var rightMostLocation =
665 connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
666 connection.Locations[0] :
667 connection.Locations[1];
671 if (rightMostLocation.Connections.All(c => c.OtherLocation(rightMostLocation).MapPosition.X < rightMostLocation.MapPosition.X))
673 Location closestLocation =
null;
674 float closestDist =
float.PositiveInfinity;
675 foreach (Location otherLocation
in Locations)
677 if (otherLocation == rightMostLocation || otherLocation.MapPosition.X < rightMostLocation.MapPosition.X) {
continue; }
678 float dist = Vector2.DistanceSquared(rightMostLocation.MapPosition, otherLocation.MapPosition);
679 if (dist < closestDist || closestLocation ==
null)
681 closestLocation = otherLocation;
686 var newConnection =
new LocationConnection(rightMostLocation, closestLocation);
687 rightMostLocation.Connections.Add(newConnection);
688 closestLocation.Connections.Add(newConnection);
690 GenerateLocationConnectionVisuals(newConnection);
697 AssignBiomes(
new MTRandom(ToolBox.StringToInt(
Seed)));
699 foreach (LocationConnection connection
in Connections)
701 if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
703 connection.Difficulty = Math.Min(connection.Locations.Min(l => l.Biome.ActualMaxDifficulty), connection.Biome.AdjustedMaxDifficulty);
707 connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
712 Location startLocation =
Locations.MinBy(l => l.MapPosition.X);
713 if (LocationType.Prefabs.TryGet(
"outpost", out LocationType startLocationType))
715 startLocation.ChangeType(campaign, startLocationType, createStores:
false);
720 location.LevelData =
new LevelData(location,
this, CalculateDifficulty(location.MapPosition.X, location.Biome));
721 location.TryAssignFactionBasedOnLocationType(campaign);
722 if (location.Type.HasOutpost && campaign !=
null && location.Type.OutpostTeam ==
CharacterTeamType.FriendlyNPC)
724 if (location.Type.Faction.IsEmpty)
727 location.Faction ??= campaign.
GetRandomFaction(Rand.RandSync.ServerAndClient);
729 if (location.Type.SecondaryFaction.IsEmpty)
735 location.CreateStores(force:
true);
738 foreach (LocationConnection connection
in Connections)
740 connection.LevelData =
new LevelData(connection);
743 CreateEndLocation(campaign);
745 float CalculateDifficulty(
float mapPosition, Biome biome)
747 float settingsFactor = campaign.
Settings.LevelDifficultyMultiplier;
748 float minDifficulty = 0;
749 float maxDifficulty = 100;
750 float difficulty = mapPosition /
Width * 100;
751 System.Diagnostics.Debug.Assert(biome !=
null);
754 minDifficulty = biome.MinDifficulty;
755 maxDifficulty = biome.AdjustedMaxDifficulty;
756 float diff = 1 - settingsFactor;
757 difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
759 return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
763 partial
void GenerateAllLocationConnectionVisuals();
765 partial
void GenerateLocationConnectionVisuals(LocationConnection connection);
769 float zoneWidth =
Width / generationParams.DifficultyZones;
770 return MathHelper.Clamp((
int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
780 float zoneWidth =
Width / generationParams.DifficultyZones;
781 int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
782 zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
783 return Biome.
Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
786 private void AssignBiomes(Random rand)
789 float zoneWidth =
Width / generationParams.DifficultyZones;
791 List<Biome> allowedBiomes =
new List<Biome>(10);
792 for (
int i = 0; i < generationParams.DifficultyZones; i++)
794 allowedBiomes.Clear();
795 allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
796 float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
802 location.
Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
806 foreach (LocationConnection connection
in Connections)
808 if (connection.Biome !=
null) {
continue; }
809 connection.
Biome = connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? connection.Locations[0].Biome : connection.Locations[1].Biome;
812 System.Diagnostics.Debug.Assert(
Locations.All(l => l.Biome !=
null));
813 System.Diagnostics.Debug.Assert(
Connections.All(c => c.Biome !=
null));
816 private Location GetPreviousToEndLocation()
818 Location previousToEndLocation =
null;
821 if (!location.Biome.IsEndBiome && (previousToEndLocation ==
null || location.MapPosition.X > previousToEndLocation.MapPosition.X))
823 previousToEndLocation = location;
826 return previousToEndLocation;
829 private void ForceLocationTypeToNone(CampaignMode campaign, Location location)
831 if (LocationType.Prefabs.TryGet(
"none", out LocationType locationType))
833 location.ChangeType(campaign, locationType, createStores:
false);
835 location.DisallowLocationTypeChanges =
true;
838 private void CreateEndLocation(CampaignMode campaign)
840 float zoneWidth =
Width / generationParams.DifficultyZones;
841 Vector2 endPos =
new Vector2(
Width - zoneWidth * 0.7f,
Height / 2);
842 float closestDist =
float.MaxValue;
846 float dist = Vector2.DistanceSquared(endPos, location.MapPosition);
847 if (location.Biome.IsEndBiome && dist < closestDist)
849 endLocation = location;
854 var previousToEndLocation = GetPreviousToEndLocation();
855 if (endLocation ==
null || previousToEndLocation ==
null) {
return; }
857 endLocations =
new List<Location>() { endLocation };
858 if (endLocation.Biome.EndBiomeLocationCount > 1)
860 FindConnectedEndLocations(endLocation);
862 void FindConnectedEndLocations(Location currLocation)
864 if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) {
return; }
865 foreach (var connection
in currLocation.Connections)
867 if (connection.Biome != endLocation.Biome) {
continue; }
868 var otherLocation = connection.OtherLocation(currLocation);
869 if (otherLocation !=
null && !endLocations.Contains(otherLocation))
871 if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) {
return; }
872 endLocations.Add(otherLocation);
873 FindConnectedEndLocations(otherLocation);
879 ForceLocationTypeToNone(campaign, previousToEndLocation);
882 for (
int i =
Locations.Count - 1; i >= 0; i--)
889 var connection =
Locations[i].Connections[j];
890 var otherLocation = connection.OtherLocation(
Locations[i]);
892 otherLocation?.Connections.Remove(connection);
895 if (!endLocations.Contains(
Locations[i]))
903 if (previousToEndLocation.Connections.None())
908 if (!location.Biome.IsEndBiome && location != previousToEndLocation && location.MapPosition.X > connectTo.MapPosition.X)
910 connectTo = location;
913 var newConnection =
new LocationConnection(previousToEndLocation, connectTo)
915 Biome = endLocation.Biome,
918 newConnection.LevelData =
new LevelData(newConnection);
920 previousToEndLocation.Connections.Add(newConnection);
921 connectTo.Connections.Add(newConnection);
924 var endConnection =
new LocationConnection(previousToEndLocation, endLocation)
926 Biome = endLocation.Biome,
929 endConnection.LevelData =
new LevelData(endConnection);
931 previousToEndLocation.Connections.Add(endConnection);
932 endLocation.Connections.Add(endConnection);
934 AssignEndLocationLevelData();
937 private void AssignEndLocationLevelData()
939 for (
int i = 0; i < endLocations.Count; i++)
941 endLocations[i].LevelData.ReassignGenerationParams(
Seed);
942 var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
943 if (outpostParams !=
null)
945 endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams;
950 private void ExpandBiomes(List<LocationConnection> seeds)
952 List<LocationConnection> nextSeeds =
new List<LocationConnection>();
953 foreach (LocationConnection connection
in seeds)
955 foreach (Location location
in connection.Locations)
957 foreach (LocationConnection otherConnection
in location.Connections)
959 if (otherConnection == connection)
continue;
960 if (otherConnection.Biome !=
null)
continue;
962 otherConnection.Biome = connection.Biome;
963 nextSeeds.Add(otherConnection);
968 if (nextSeeds.Count > 0)
970 ExpandBiomes(nextSeeds);
975 #endregion Generation
989 DebugConsole.ThrowError(
"Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
998 if (currentEndLocationIndex < endLocations.Count - 1)
1011 DebugConsole.ThrowError(
"Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace());
1043 CurrentLocation =
null;
1047 if (index < 0 || index >= Locations.Count)
1049 DebugConsole.ThrowError(
"Location index out of bounds");
1053 Location prevLocation = CurrentLocation;
1054 CurrentLocation = Locations[index];
1055 Discover(CurrentLocation);
1057 CurrentLocation.CreateStores();
1058 if (prevLocation != CurrentLocation)
1060 var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
1061 if (connection !=
null)
1063 connection.Passed =
true;
1073 SelectedLocation =
null;
1074 SelectedConnection =
null;
1076 OnLocationSelected?.Invoke(
null,
null);
1080 if (index < 0 || index >= Locations.Count)
1082 DebugConsole.ThrowError(
"Location index out of bounds");
1086 Location prevSelected = SelectedLocation;
1087 SelectedLocation = Locations[index];
1089 if (currentDisplayLocation == SelectedLocation)
1091 SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1095 SelectedConnection =
1096 Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
1097 Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1099 if (SelectedConnection?.Locked ??
false)
1102 $
"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
1103 $
" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
1104 + Environment.StackTrace.CleanupStackTrace();
1105 GameAnalyticsManager.AddErrorEventOnce(
"MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1106 DebugConsole.ThrowError(errorMsg);
1108 if (prevSelected != SelectedLocation)
1110 OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1116 if (!Locations.Contains(location))
1118 string errorMsg = $
"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
1119 DebugConsole.ThrowError(errorMsg);
1120 GameAnalyticsManager.AddErrorEventOnce(
"Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1124 Location prevSelected = SelectedLocation;
1125 SelectedLocation = location;
1126 SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
1127 if (SelectedConnection?.Locked ??
false)
1129 DebugConsole.ThrowError(
"A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
1131 if (prevSelected != SelectedLocation)
1133 OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
1139 if (CurrentLocation ==
null)
1141 string errorMsg =
"Failed to select a mission (current location not set).";
1142 DebugConsole.ThrowError(errorMsg);
1143 GameAnalyticsManager.AddErrorEventOnce(
"Map.SelectMission:CurrentLocationNotSet", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1147 if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
1149 CurrentLocation.SetSelectedMissionIndices(missionIndices);
1150 foreach (
Mission selectedMission
in CurrentLocation.SelectedMissions.ToList())
1152 if (selectedMission.
Locations[0] != CurrentLocation ||
1153 selectedMission.
Locations[1] != CurrentLocation)
1155 if (SelectedConnection ==
null) {
return; }
1157 if (selectedMission.
Locations[1] != SelectedLocation)
1159 CurrentLocation.DeselectMission(selectedMission);
1163 OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
1169 List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
1170 List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
1172 if (undiscoveredLocations.Count > 0 && preferUndiscovered)
1174 SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
1178 SelectLocation(nextLocations[Rand.Int(nextLocations.Count, Rand.RandSync.Unsynced)]);
1185 int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f));
1190 steps = Math.Max(1, steps);
1192 steps = Math.Min(steps, 5);
1193 for (
int i = 0; i < steps; i++)
1195 ProgressWorld(campaign);
1199 for (
int i = 0; i < Math.Max(1, steps); i++)
1201 foreach (
Location location
in Locations)
1208 Radiation?.OnStep(steps);
1213 foreach (
Location location
in Locations)
1218 if (furthestDiscoveredLocation ==
null ||
1221 furthestDiscoveredLocation = location;
1225 foreach (LocationConnection connection
in Connections)
1230 foreach (Location location
in Locations)
1232 if (location.MapPosition.X > furthestDiscoveredLocation.
MapPosition.X)
1237 if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) {
continue; }
1239 if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered)
1241 location.UpdateStores();
1246 private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location)
1248 location.TimeSinceLastTypeChange++;
1249 location.LocationTypeChangeCooldown--;
1251 if (location.PendingLocationTypeChange !=
null)
1253 if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
1256 location.PendingLocationTypeChange =
null;
1260 location.PendingLocationTypeChange =
1261 (location.PendingLocationTypeChange.Value.typeChange,
1262 location.PendingLocationTypeChange.Value.delay - 1,
1263 location.PendingLocationTypeChange.Value.parentMission);
1264 if (location.PendingLocationTypeChange.Value.delay <= 0)
1266 return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
1272 Dictionary<LocationTypeChange, float> allowedTypeChanges =
new Dictionary<LocationTypeChange, float>();
1273 foreach (LocationTypeChange typeChange
in location.Type.CanChangeTo)
1275 float probability = typeChange.DetermineProbability(location);
1276 if (probability <= 0.0f) {
continue; }
1277 allowedTypeChanges.Add(typeChange, probability);
1281 if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
1283 var selectedTypeChange =
1284 ToolBox.SelectWeightedRandom(
1285 allowedTypeChanges.Keys.ToList(),
1286 allowedTypeChanges.Values.ToList(),
1287 Rand.RandSync.Unsynced);
1288 if (selectedTypeChange !=
null)
1290 if (selectedTypeChange.RequiredDurationRange.X > 0)
1292 location.PendingLocationTypeChange =
1293 (selectedTypeChange,
1294 Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
1299 return ChangeLocationType(campaign, location, selectedTypeChange);
1305 foreach (LocationTypeChange typeChange
in location.Type.CanChangeTo)
1307 foreach (var requirement
in typeChange.Requirements)
1309 if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
1311 if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
1312 location.ProximityTimer[requirement] += 1;
1316 location.ProximityTimer.Remove(requirement);
1324 private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
1326 LocalizedString prevName = location.DisplayName;
1328 if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
1330 DebugConsole.ThrowError($
"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
1334 if (location.LocationTypeChangesBlocked) {
return false; }
1336 if (newType.OutpostTeam != location.Type.OutpostTeam ||
1337 newType.HasOutpost != location.Type.HasOutpost)
1339 location.ClearMissions();
1341 location.ChangeType(campaign, newType);
1342 ChangeLocationTypeProjSpecific(location, prevName, change);
1343 foreach (var requirement
in change.Requirements)
1345 location.ProximityTimer.Remove(requirement);
1347 location.TimeSinceLastTypeChange = 0;
1348 location.LocationTypeChangeCooldown = change.CooldownAfterChange;
1349 location.PendingLocationTypeChange =
null;
1355 return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance;
1365 var locationsToTest =
new List<Location>() { startLocation };
1366 var nextBatchToTest =
new HashSet<Location>();
1367 var checkedLocations =
new HashSet<Location>();
1368 while (locationsToTest.Any())
1370 foreach (var location
in locationsToTest)
1372 checkedLocations.Add(location);
1373 if (criteria(location)) {
return distance; }
1374 foreach (var connection
in location.Connections)
1376 if (connectionCriteria !=
null && connectionCriteria(connection))
1380 var otherLocation = connection.OtherLocation(location);
1381 if (!checkedLocations.Contains(otherLocation))
1383 nextBatchToTest.Add(otherLocation);
1386 if (distance > maxDistance) {
return int.MaxValue; }
1389 locationsToTest.Clear();
1390 locationsToTest.AddRange(nextBatchToTest);
1391 nextBatchToTest.Clear();
1393 return int.MaxValue;
1399 partial
void ClearAnimQueue();
1403 if (location is
null) {
return; }
1404 if (locationsDiscovered.Contains(location)) {
return; }
1405 locationsDiscovered.Add(location);
1414 if (location is
null) {
return; }
1415 if (locationsVisited.Contains(location)) {
return; }
1416 locationsVisited.Add(location);
1417 RemoveFogOfWarProjSpecific(location);
1422 locationsDiscovered.Clear();
1423 locationsVisited.Clear();
1428 if (!trackedLocationDiscoveryAndVisitOrder) {
return null; }
1429 if (location is
null) {
return -1; }
1430 return locationsDiscovered.IndexOf(location);
1435 if (!trackedLocationDiscoveryAndVisitOrder) {
return null; }
1436 if (location is
null) {
return -1; }
1437 int index = locationsVisited.IndexOf(location);
1438 if (includeLocationsWithoutOutpost) {
return index; }
1439 int noOutpostLocations = 0;
1440 for (
int i = 0; i < index; i++)
1442 if (locationsVisited[i] is not
Location l) {
continue; }
1443 if (l.HasOutpost()) {
continue; }
1444 noOutpostLocations++;
1446 return index - noOutpostLocations;
1451 if (location is
null) {
return false; }
1452 return locationsDiscovered.Contains(location);
1457 if (location is
null) {
return false; }
1458 return locationsVisited.Contains(location);
1461 partial
void RemoveFogOfWarProjSpecific(
Location location);
1468 Map map =
new Map(campaign, element);
1469 map.
LoadState(campaign, element,
false);
1482 SetLocation(element.GetAttributeInt(
"currentlocation", 0));
1484 if (!Version.TryParse(element.GetAttributeString(
"version",
""), out Version version))
1486 DebugConsole.ThrowError(
"Incompatible map save file, loading the game failed.");
1490 ClearLocationHistory();
1491 foreach (var subElement
in element.Elements())
1493 switch (subElement.Name.ToString().ToLowerInvariant())
1496 int locationIndex = subElement.GetAttributeInt(
"i", -1);
1497 if (locationIndex < 0 || locationIndex >= Locations.Count)
1499 DebugConsole.AddWarning($
"Error while loading the campaign map: location index out of bounds ({locationIndex})");
1502 Location location = Locations[locationIndex];
1506 for (
int j = 0; j < location.
Type.
CanChangeTo[i].Requirements.Count; j++)
1515 if (subElement.GetAttributeBool(
"discovered",
false))
1519 trackedLocationDiscoveryAndVisitOrder =
false;
1522 Identifier locationType = subElement.GetAttributeIdentifier(
"type", Identifier.Empty);
1526 location.
ChangeType(campaign, newLocationType);
1528 var factionIdentifier = subElement.GetAttributeIdentifier(
"faction", Identifier.Empty);
1529 location.
Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1531 if (showNotifications && prevLocationType != location.
Type)
1536 ChangeLocationTypeProjSpecific(location, prevLocationName, change);
1541 var secondaryFactionIdentifier = subElement.GetAttributeIdentifier(
"secondaryfaction", Identifier.Empty);
1542 location.
SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier);
1550 if (subElement.Attribute(
"i") ==
null) {
continue; }
1552 int connectionIndex = subElement.GetAttributeInt(
"i", -1);
1553 if (connectionIndex < 0 || connectionIndex >= Connections.Count)
1555 DebugConsole.AddWarning($
"Error while loading the campaign map: connection index out of bounds ({connectionIndex})");
1558 Connections[connectionIndex].Passed = subElement.GetAttributeBool(
"passed",
false);
1559 Connections[connectionIndex].Locked = subElement.GetAttributeBool(
"locked",
false);
1562 Radiation =
new Radiation(
this, generationParams.RadiationParams, subElement);
1565 bool trackedVisitedEmptyLocations = subElement.GetAttributeBool(
"trackedvisitedemptylocations",
false);
1566 foreach (var childElement
in subElement.GetChildElements(
"location"))
1568 if (GetLocation(childElement) is
Location l)
1571 if (!trackedVisitedEmptyLocations)
1573 if (!l.HasOutpost())
1577 trackedLocationDiscoveryAndVisitOrder =
false;
1583 foreach (var childElement
in subElement.GetChildElements(
"location"))
1585 if (GetLocation(childElement) is
Location l)
1593 Location GetLocation(XElement element)
1595 int index = element.GetAttributeInt(
"i", -1);
1596 if (index < 0) {
return null; }
1597 return Locations[index];
1604 this.Discover(location, checkTalents:
false);
1605 if (furthestDiscoveredLocation ==
null || location.
MapPosition.X > furthestDiscoveredLocation.
MapPosition.X)
1607 furthestDiscoveredLocation = location;
1611 foreach (
Location location
in Locations)
1618 if (version <
new Version(1, 0) && Locations.None(l => l.Faction !=
null || l.SecondaryFaction !=
null))
1620 Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
1621 foreach (
Location location
in Locations)
1625 location.
Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
1626 if (location != StartLocation)
1628 location.
SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient);
1634 int currentLocationConnection = element.GetAttributeInt(
"currentlocationconnection", -1);
1635 if (currentLocationConnection >= 0)
1637 Connections[currentLocationConnection].Locked =
false;
1638 SelectLocation(Connections[currentLocationConnection].OtherLocation(CurrentLocation));
1643 if (CurrentLocation !=
null && !CurrentLocation.Type.HasOutpost && SelectedConnection ==
null)
1645 DebugConsole.AddWarning($
"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
1646 SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
1650 var previousToEndLocation = GetPreviousToEndLocation();
1651 if (previousToEndLocation !=
null)
1653 ForceLocationTypeToNone(campaign, previousToEndLocation);
1659 XElement mapElement =
new XElement(
"map");
1661 mapElement.Add(
new XAttribute(
"version",
GameMain.
Version.ToString()));
1662 mapElement.Add(
new XAttribute(
"currentlocation", CurrentLocationIndex));
1665 if (campaign.NextLevel !=
null && campaign.NextLevel.Type ==
LevelData.
LevelType.LocationConnection)
1667 mapElement.Add(
new XAttribute(
"currentlocationconnection", Connections.IndexOf(CurrentLocation.Connections.Find(c => c.LevelData == campaign.NextLevel))));
1671 mapElement.Add(
new XAttribute(
"currentlocationconnection", Connections.IndexOf(Connections.Find(c => c.LevelData ==
Level.
Loaded.
LevelData))));
1674 mapElement.Add(
new XAttribute(
"width", Width));
1675 mapElement.Add(
new XAttribute(
"height", Height));
1676 mapElement.Add(
new XAttribute(
"selectedlocation", SelectedLocationIndex));
1677 mapElement.Add(
new XAttribute(
"startlocation", Locations.IndexOf(StartLocation)));
1678 mapElement.Add(
new XAttribute(
"endlocations",
string.Join(
',', EndLocations.Select(e => Locations.IndexOf(e)))));
1679 mapElement.Add(
new XAttribute(
"seed", Seed));
1681 for (
int i = 0; i < Locations.Count; i++)
1683 var location = Locations[i];
1684 var locationElement = location.
Save(
this, mapElement);
1685 locationElement.Add(
new XAttribute(
"i", i));
1688 for (
int i = 0; i < Connections.Count; i++)
1690 var connection = Connections[i];
1692 var connectionElement =
new XElement(
"connection",
1693 new XAttribute(
"passed", connection.Passed),
1694 new XAttribute(
"locked", connection.Locked),
1695 new XAttribute(
"difficulty", connection.Difficulty),
1696 new XAttribute(
"biome", connection.Biome.Identifier),
1697 new XAttribute(
"i", i),
1698 new XAttribute(
"locations", Locations.IndexOf(connection.Locations[0]) +
"," + Locations.IndexOf(connection.Locations[1])));
1699 connection.LevelData.Save(connectionElement);
1700 mapElement.Add(connectionElement);
1703 if (Radiation !=
null)
1705 mapElement.Add(Radiation.Save());
1708 if (locationsDiscovered.Any())
1710 var discoveryElement =
new XElement(
"discovered",
1711 new XAttribute(
"trackedvisitedemptylocations",
true));
1712 foreach (
Location location
in locationsDiscovered)
1714 int index = Locations.IndexOf(location);
1715 var locationElement =
new XElement(
"location",
new XAttribute(
"i", index));
1716 discoveryElement.Add(locationElement);
1718 mapElement.Add(discoveryElement);
1721 if (locationsVisited.Any())
1723 var visitElement =
new XElement(
"visited");
1724 foreach (
Location location
in locationsVisited)
1726 int index = Locations.IndexOf(location);
1727 var locationElement =
new XElement(
"location",
new XAttribute(
"i", index));
1728 visitElement.Add(locationElement);
1730 mapElement.Add(visitElement);
1733 element.Add(mapElement);
1738 foreach (
Location location
in Locations)
1742 RemoveProjSpecific();
1745 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 readonly Version Version
static GameSession GameSession
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
int GetZoneIndex(float xPos)
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