6 using FarseerPhysics.Dynamics;
7 using Microsoft.Xna.Framework;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Globalization;
13 using System.Xml.Linq;
27 SingleDestructibleWall,
28 GlobalDestructibleWall
55 private static Level loaded;
58 get {
return loaded; }
61 if (loaded == value) {
return; }
63 GameAnalyticsManager.SetCurrentLevel(loaded?.
LevelData);
137 MainPath, SidePath,
Cave
171 Nodes =
new List<Point>(nodes);
172 Cells =
new List<VoronoiCell>();
181 public readonly List<Tunnel>
Tunnels =
new List<Tunnel>();
201 private List<VoronoiCell>[,] cellGrid;
202 private List<VoronoiCell> cells;
223 public readonly List<VoronoiCell>
Cells;
227 Debug.Assert(cells !=
null && cells.Any());
240 private Point startPosition, endPosition;
244 private List<Body> bodies;
246 private List<Point> bottomPositions;
250 const float NetworkUpdateInterval = 5.0f;
251 private float networkUpdateTimer;
255 get {
return startPosition.ToVector2(); }
258 private Point startExitPosition;
261 get {
return startExitPosition.ToVector2(); }
271 get {
return endPosition.ToVector2(); }
274 private Point endExitPosition;
277 get {
return endExitPosition.ToVector2(); }
318 public List<Ruin>
Ruins {
get;
private set; }
320 public List<Submarine>
Wrecks {
get;
private set; }
323 private Sonar beaconSonar;
329 public List<Tunnel>
Tunnels {
get;
private set; } =
new List<Tunnel>();
331 public List<Cave>
Caves {
get;
private set; } =
new List<Cave>();
370 private readonly Dictionary<LevelGenStage, int> equalityCheckValues = Enum.GetValues(typeof(
LevelGenStage))
378 equalityCheckValues[stage] = Rand.Int(
int.MaxValue, Rand.RandSync.ServerAndClient);
381 private void SetEqualityCheckValue(
LevelGenStage stage,
int value)
383 equalityCheckValues[stage] = value;
386 private void ClearEqualityCheckValues()
390 equalityCheckValues[stage] = 0;
500 Debug.Assert(levelData.
Biome !=
null);
501 if (levelData.
Biome ==
null) {
throw new ArgumentException(
"Biome was null"); }
502 if (levelData.
Size.X <= 0) {
throw new ArgumentException(
"Level width needs to be larger than zero."); }
503 if (levelData.
Size.Y <= 0) {
throw new ArgumentException(
"Level height needs to be larger than zero."); }
507 preSelectedStartOutpost = startOutpost,
508 preSelectedEndOutpost = endOutpost
510 level.
Generate(mirror, startLocation, endLocation);
525 ClearEqualityCheckValues();
532 Rand.SetSyncedSeed(ToolBox.StringToInt(
Seed));
536 SetEqualityCheckValue(
LevelGenStage.Size, borders.Width ^ borders.Height << 16);
544 if (backgroundCreatureManager ==
null)
546 var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
550 Stopwatch sw =
new Stopwatch();
556 bodies =
new List<Body>();
557 List<Vector2> sites =
new List<Vector2>();
562 renderer =
new LevelRenderer(
this);
572 dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f);
573 minWidth = Math.Max(dockedSubBorders.Width, dockedSubBorders.Height);
574 minMainPathWidth = Math.Max(minMainPathWidth, minWidth);
577 minMainPathWidth = Math.Min(minMainPathWidth, borders.Width / 5);
583 -Math.Min(minMainPathWidth * 2, borders.Height / 5));
585 if (pathBorders.Width <= 0) {
throw new InvalidOperationException($
"The width of the level's path area is invalid ({pathBorders.Width})"); }
586 if (pathBorders.Height <= 0) {
throw new InvalidOperationException($
"The height of the level's path area is invalid ({pathBorders.Height})"); }
588 startPosition =
new Point(
591 startExitPosition =
new Point(startPosition.X, borders.Bottom);
593 endPosition =
new Point(
596 endExitPosition =
new Point(endPosition.X, borders.Bottom);
604 Tunnel mainPath =
new Tunnel(
607 minMainPathWidth, parentTunnel:
null);
610 Tunnel startPath =
null, endPath =
null, endHole =
null;
613 startPath =
new Tunnel(
615 new List<Point>() { startExitPosition, startPosition },
616 minWidth, parentTunnel: mainPath);
621 startExitPosition = startPosition;
625 endPath =
new Tunnel(
627 new List<Point>() { endPosition, endExitPosition },
628 minWidth, parentTunnel: mainPath);
633 endExitPosition = endPosition;
640 endHole =
new Tunnel(
642 new List<Point>() { startPosition, new Point(0, startPosition.Y) },
643 minWidth, parentTunnel: mainPath);
647 endHole =
new Tunnel(
649 new List<Point>() { endPosition, new Point(Size.X, endPosition.Y) },
650 minWidth, parentTunnel: mainPath);
657 Tunnel abyssTunnel =
null;
660 Point lowestPoint = mainPath.Nodes.First();
661 foreach (var pathNode
in mainPath.Nodes)
663 if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; }
665 abyssTunnel =
new Tunnel(
667 new List<Point>() { lowestPoint, new Point(lowestPoint.X, 0) },
668 minWidth, parentTunnel: mainPath);
674 for (
int j = 0; j < sideTunnelCount; j++)
676 if (mainPath.Nodes.Count < 4) {
break; }
677 var validTunnels =
Tunnels.FindAll(t => t.Type !=
TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
679 Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)];
680 if (tunnelToBranchOff ==
null) { tunnelToBranchOff = mainPath; }
682 Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)];
683 Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)];
688 Tunnels.Add(
new Tunnel(
TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
691 CalculateTunnelDistanceField(
null);
692 GenerateSeaFloorPositions();
700 GenerateCaves(mainPath);
708 GenerateVoronoiSites();
716 Stopwatch sw2 =
new Stopwatch();
719 Debug.Assert(
siteCoordsX.Count == siteCoordsY.Count);
721 int remainingRetries = 5;
722 bool voronoiGraphInvalid =
false;
726 voronoiGraphInvalid =
false;
728 List<GraphEdge> graphEdges = voronoi.
MakeVoronoiGraph(
siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
729 cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders,
GridCellSize, out cellGrid);
730 for (
int i = 0; i < cells.Count; i++)
732 for (
int j = i + 1; j < cells.Count; j++)
743 if (cells[j].IsPointInside(cells[i].Center))
745 voronoiGraphInvalid =
true;
748 if (voronoiGraphInvalid) {
break; }
751 if (voronoiGraphInvalid)
753 string errorMsg =
"Unknown error during level generation. Invalid voronoi graph: the same voronoi site was inside multiple cells.";
754 if (remainingRetries > 0)
756 DebugConsole.AddWarning(errorMsg +
" Retrying...");
757 GenerateVoronoiSites();
762 DebugConsole.ThrowError(errorMsg);
765 }
while (remainingRetries > 0 && voronoiGraphInvalid);
767 GenerateAbyssGeometry();
768 GenerateAbyssPositions();
770 Debug.WriteLine(
"find cells: " + sw2.ElapsedMilliseconds +
" ms");
777 List<VoronoiCell> pathCells =
new List<VoronoiCell>();
778 foreach (Tunnel tunnel
in Tunnels)
780 CaveGenerator.GeneratePath(tunnel,
this);
783 if (tunnel != startPath && tunnel != endPath && tunnel != endHole)
785 var distinctCells = tunnel.Cells.Distinct().ToList();
786 for (
int i = 2; i < distinctCells.Count; i += 3)
789 new Point((
int)distinctCells[i].
Site.
Coord.
X, (
int)distinctCells[i].Site.Coord.Y),
790 tunnel.Type ==
TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath,
791 Caves.Find(cave => cave.Tunnels.Contains(tunnel))));
796 bool connectToParentTunnel = tunnel.Type !=
TunnelType.Cave || tunnel.ParentTunnel.Type ==
TunnelType.Cave;
797 GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel :
null);
799 EnlargePath(tunnel.Cells, tunnel.MinWidth);
800 foreach (var pathCell
in tunnel.Cells)
802 MarkEdges(pathCell, tunnel.Type);
803 if (!pathCells.Contains(pathCell))
805 pathCells.Add(pathCell);
816 edge.NextToMainPath =
true;
819 edge.NextToSidePath =
true;
829 var potentialIslands =
new List<VoronoiCell>();
830 foreach (var cell
in pathCells)
832 if (GetDistToTunnel(cell.
Center, mainPath) < minMainPathWidth ||
833 (startPath !=
null && GetDistToTunnel(cell.
Center, startPath) < minMainPathWidth) ||
834 (endPath !=
null && GetDistToTunnel(cell.
Center, endPath) < minMainPathWidth) ||
835 (endHole !=
null && GetDistToTunnel(cell.
Center, endHole) < minMainPathWidth)) {
continue; }
836 if (cell.
Edges.Any(e => e.AdjacentCell(cell)?.CellType !=
CellType.Path || e.NextToCave)) {
continue; }
838 potentialIslands.Add(cell);
842 if (potentialIslands.Count == 0) {
break; }
843 var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient);
845 island.Island =
true;
846 pathCells.Remove(island);
851 WayPoint wayPoint =
new WayPoint(
852 positionOfInterest.Position.ToVector2(),
857 startPosition.X = (int)pathCells[0].
Site.
Coord.
X;
858 startExitPosition.X = startPosition.X;
868 cells.ForEach(c => c.CellType =
CellType.Removed);
872 cells = cells.Except(pathCells).ToList();
874 cells.ForEachMod(c =>
876 if (c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y,
Size.Y) && e.AdjacentCell(c) ==
null))
883 int xPadding = borders.Width / 5;
888 if (cell.
Site.
Coord.Y < borders.Height / 2) {
continue; }
889 cell.
Edges.ForEach(e => e.OutsideLevel =
true);
894 abyssIsland.Cells.RemoveAll(c => c.CellType ==
CellType.Path);
895 cells.AddRange(abyssIsland.Cells);
898 List<Point> ruinPositions =
new List<Point>();
903 bool hasRuinMissions = GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ??
false;
906 ruinCount = Math.Max(ruinCount, 1);
909 for (
int i = 0; i < ruinCount; i++)
911 Point ruinSize =
new Point(5000);
912 int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
913 int limitRight = Math.Min(endPosition.X,
Size.X - ruinSize.X / 2);
915 Debug.Assert(limits.Width > 0);
916 Debug.Assert(limits.Height > 0);
917 ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible:
true, limits: limits));
918 CalculateTunnelDistanceField(ruinPositions);
927 cell.
Edges.ForEach(e => e.OutsideLevel =
false);
932 for (
int x = 0; x < cellGrid.GetLength(0); x++)
934 for (
int y = 0; y < cellGrid.GetLength(1); y++)
936 cellGrid[x, y].Clear();
946 HashSet<GraphEdge> mirroredEdges =
new HashSet<GraphEdge>();
947 HashSet<Site> mirroredSites =
new HashSet<Site>();
948 List<VoronoiCell> allCells =
new List<VoronoiCell>(cells);
949 allCells.AddRange(pathCells);
954 if (mirroredEdges.Contains(edge)) {
continue; }
956 edge.Point2.X = borders.Width - edge.Point2.X;
957 if (edge.
Site1 !=
null && !mirroredSites.Contains(edge.
Site1))
963 mirroredSites.Add(edge.
Site1);
965 if (edge.Site2 !=
null && !mirroredSites.Contains(edge.Site2))
969 edge.Site2.
Coord.
X = borders.Width - edge.Site2.
Coord.
X;
970 mirroredSites.Add(edge.Site2);
972 mirroredEdges.Add(edge);
978 island.Area =
new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height);
979 foreach (var cell
in island.Cells)
981 if (!mirroredSites.Contains(cell.Site))
984 cell.Site.Coord.X %
GridCellSize >= 0.0f) { cell.Site.Coord.X += 1.0f; }
985 cell.Site.Coord.X = borders.Width - cell.Site.Coord.X;
986 mirroredSites.Add(cell.Site);
991 for (
int i = 0; i < ruinPositions.Count; i++)
993 ruinPositions[i] =
new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y);
996 foreach (Cave cave
in Caves)
998 cave.Area =
new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height);
999 cave.StartPos =
new Point(borders.Width - cave.StartPos.X, cave.StartPos.Y);
1000 cave.EndPos =
new Point(borders.Width - cave.EndPos.X, cave.EndPos.Y);
1003 foreach (Tunnel tunnel
in Tunnels)
1005 for (
int i = 0; i < tunnel.Nodes.Count; i++)
1007 tunnel.Nodes[i] =
new Point(borders.Width - tunnel.Nodes[i].X, tunnel.Nodes[i].Y);
1023 foreach (WayPoint waypoint
in WayPoint.WayPointList)
1025 if (waypoint.Submarine !=
null)
continue;
1026 waypoint.Move(
new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f));
1029 for (
int i = 0; i < bottomPositions.Count; i++)
1031 bottomPositions[i] =
new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y);
1033 bottomPositions.Reverse();
1035 startPosition.X = borders.Width - startPosition.X;
1036 endPosition.X = borders.Width - endPosition.X;
1038 startExitPosition.X = borders.Width - startExitPosition.X;
1039 endExitPosition.X = borders.Width - endExitPosition.X;
1041 CalculateTunnelDistanceField(ruinPositions);
1047 x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1);
1049 y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1);
1051 cellGrid[x, y].Add(cell);
1055 foreach (Cave cave
in Caves)
1057 if (cave.Area.Y > 0)
1059 List<VoronoiCell> cavePathCells = CreatePathToClosestTunnel(cave.StartPos);
1061 var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type !=
TunnelType.Cave);
1063 WayPoint prevWp = mainTunnel.WayPoints.First();
1066 for (
int i = 0; i < cavePathCells.Count; i++)
1068 var connectingEdge = i > 0 ? cavePathCells[i].Edges.Find(e => e.AdjacentCell(cavePathCells[i]) == cavePathCells[i - 1]) :
null;
1069 if (connectingEdge !=
null)
1071 var edgeWayPoint =
new WayPoint(connectingEdge.Center,
SpawnType.Path, submarine:
null);
1072 ConnectWaypoints(prevWp, edgeWayPoint, 500.0f);
1073 prevWp = edgeWayPoint;
1075 var newWaypoint =
new WayPoint(cavePathCells[i].Center,
SpawnType.Path, submarine:
null);
1076 ConnectWaypoints(prevWp, newWaypoint, 500.0f);
1077 prevWp = newWaypoint;
1079 var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints);
1080 ConnectWaypoints(prevWp, closestPathPoint, 500.0f);
1084 List<VoronoiCell> caveCells =
new List<VoronoiCell>();
1085 caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
1086 foreach (var caveCell
in caveCells)
1088 if (
PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) {
continue; }
1089 if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
1091 var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
1094 chunk.Body.BodyType = BodyType.Static;
1107 Ruins =
new List<Ruin>();
1108 for (
int i = 0; i < ruinPositions.Count; i++)
1110 Rand.SetSyncedSeed(ToolBox.StringToInt(
Seed) + i);
1111 GenerateRuin(ruinPositions[i], mirror, hasRuinMissions);
1122 List<Point> iceChunkPositions =
new List<Point>();
1125 if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) {
continue; }
1126 if (pos.Position.X < pathBorders.X + minMainPathWidth || pos.Position.X > pathBorders.Right - minMainPathWidth) {
continue; }
1127 if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) {
continue; }
1128 if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) {
continue; }
1129 iceChunkPositions.Add(pos.Position);
1134 if (iceChunkPositions.Count == 0) {
break; }
1135 Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)];
1136 float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient);
1137 var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f);
1138 var chunk = CreateIceChunk(vertices, selectedPos.ToVector2());
1139 chunk.MoveAmount =
new Vector2(0.0f, minMainPathWidth * 0.7f);
1140 chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient);
1142 iceChunkPositions.Remove(selectedPos);
1157 ge.
IsSolid = adjacentCell ==
null || !cells.Contains(adjacentCell);
1161 List<VoronoiCell> cellsWithBody =
new List<VoronoiCell>(cells);
1166 CaveGenerator.RoundCell(cell,
1174 List<(List<VoronoiCell> cells, Cave parentCave)> cellBatches =
new List<(List<VoronoiCell>, Cave)>
1176 (cellsWithBody.ToList(),
null)
1178 foreach (Cave cave
in Caves)
1180 (List<VoronoiCell> cells, Cave parentCave) newCellBatch = (
new List<VoronoiCell>(), cave);
1181 foreach (var caveCell
in cave.Tunnels.SelectMany(t => t.Cells))
1183 foreach (var edge
in caveCell.Edges)
1185 if (!edge.NextToCave) {
continue; }
1186 if (edge.Cell1?.CellType ==
CellType.Solid && !newCellBatch.cells.Contains(edge.Cell1))
1188 Debug.Assert(cellsWithBody.Contains(edge.Cell1));
1189 cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell1));
1190 newCellBatch.cells.Add(edge.Cell1);
1192 if (edge.Cell2?.CellType ==
CellType.Solid && !newCellBatch.cells.Contains(edge.Cell2))
1194 Debug.Assert(cellsWithBody.Contains(edge.Cell2));
1195 cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell2));
1196 newCellBatch.cells.Add(edge.Cell2);
1200 if (newCellBatch.cells.Any())
1202 cellBatches.Add(newCellBatch);
1205 cellBatches.RemoveAll(cb => !cb.cells.Any());
1207 int totalCellsInBatches = cellBatches.Sum(cb => cb.cells.Count);
1208 Debug.Assert(cellsWithBody.Count == totalCellsInBatches);
1210 List<List<Vector2[]>> triangleLists =
new List<List<Vector2[]>>();
1211 foreach ((List<VoronoiCell> cells, Cave cave) cellBatch in cellBatches)
1213 bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.cells,
this, out List<Vector2[]> triangles));
1214 triangleLists.Add(triangles);
1217 bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody,
this, out List<Vector2[]> triangles));
1221 CompareCCW compare =
new CompareCCW(cell.
Center);
1226 if (edge.Cell2 !=
null && edge.Cell2.
Body ==
null && edge.Cell2.
CellType !=
CellType.Empty) { edge.Cell2 =
null; }
1229 if (compare.Compare(edge.
Point1, edge.Point2) == -1)
1232 edge.
Point1 = edge.Point2;
1239 Debug.Assert(triangleLists.Count == cellBatches.Count);
1240 for (
int i = 0; i < triangleLists.Count; i++)
1243 CaveGenerator.GenerateWallVertices(triangleLists[i],
GenerationParams, zCoord: 0.9f).ToArray(),
1244 CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells,
this, zCoord: 0.9f).ToArray(),
1245 cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite ==
null ?
GenerationParams.
WallSprite.
Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture,
1246 cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite ==
null ?
GenerationParams.
WallEdgeSprite.
Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture,
1257 List<GraphEdge> usedSpireEdges =
new List<GraphEdge>();
1260 var spire = CreateIceSpire(usedSpireEdges);
1261 if (spire !=
null) {
ExtraWalls.Add(spire); };
1272 GenerateRuinWayPoints(ruin);
1275 foreach (Tunnel tunnel
in Tunnels)
1277 if (tunnel.ParentTunnel ==
null) {
continue; }
1278 if (tunnel.Type ==
TunnelType.Cave && tunnel.ParentTunnel == mainPath) {
continue; }
1279 ConnectWaypoints(tunnel, tunnel.ParentTunnel);
1295 ConvertUnits.ToSimUnits(
new Vector2(borders.X, 0)),
1296 ConvertUnits.ToSimUnits(
new Vector2(borders.Right, 0)));
1299 TopBarrier.SetTransform(ConvertUnits.ToSimUnits(
new Vector2(0.0f, borders.Height)), 0.0f);
1301 TopBarrier.CollisionCategories = Physics.CollisionLevel;
1309 Point tempP = startPosition;
1310 startPosition = endPosition;
1311 endPosition = tempP;
1313 tempP = startExitPosition;
1314 startExitPosition = endExitPosition;
1315 endExitPosition = tempP;
1320 startPosition = startExitPosition;
1325 endPosition = endExitPosition;
1329 CreateBeaconStation();
1335 GenerateEqualityCheckValue(
LevelGenStage.PlaceLevelObjects);
1341 if (sub.Info.IsOutpost)
1344 if (GameMain.GameSession?.GameMode is TutorialMode) {
continue; }
1346 OutpostGenerator.PowerUpOutpost(sub);
1368 MapEntity.MapLoaded(MapEntity.MapEntityList.FindAll(me => me.Submarine ==
null),
false);
1370 Debug.WriteLine(
"Generatelevel: " + sw2.ElapsedMilliseconds +
" ms");
1373 Debug.WriteLine(
"**********************************************************************************");
1374 Debug.WriteLine(
"Generated a map with " +
siteCoordsX.Count +
" sites in " + sw.ElapsedMilliseconds +
" ms");
1375 Debug.WriteLine(
"Seed: " +
Seed);
1376 Debug.WriteLine(
"**********************************************************************************");
1378 if (GameSettings.CurrentConfig.VerboseLogging)
1386 if (GameMain.Server.EntityEventManager.Events.Count() > 0)
1388 DebugConsole.NewMessage(
"WARNING: Entity events have been created during level generation. Events should not be created until the round is fully initialized.");
1390 GameMain.Server.EntityEventManager.Clear();
1400 private void GenerateVoronoiSites()
1403 int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
1405 siteCoordsX =
new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
1406 siteCoordsY =
new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
1407 const int caveSiteInterval = 500;
1408 for (
int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
1410 for (
int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
1412 int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
1413 int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
1415 bool closeToTunnel =
false;
1416 bool closeToCave =
false;
1417 foreach (Tunnel tunnel
in Tunnels)
1419 float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
1420 for (
int i = 1; i < tunnel.Nodes.Count; i++)
1422 if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) {
continue; }
1423 if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) {
continue; }
1424 if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) {
continue; }
1425 if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) {
continue; }
1427 double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i],
new Point(siteX, siteY));
1428 if (Math.Sqrt(tunnelDistSqr) < minDist)
1430 closeToTunnel =
true;
1444 if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) {
continue; }
1447 if (!TooCloseToOtherSites(siteX, siteY))
1450 siteCoordsY.Add(siteY);
1455 for (
int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
1457 for (
int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
1459 int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
1460 int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
1462 if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
1465 siteCoordsY.Add(caveSiteY);
1473 bool TooCloseToOtherSites(
double siteX,
double siteY,
float minDistance = 10.0f)
1475 float minDistanceSqr = minDistance * minDistance;
1478 if (MathUtils.DistanceSquared(
siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
1490 $
"Potential error in level generation: invalid voronoi site ({siteCoordsX[i]})");
1492 !
double.IsNaN(siteCoordsY[i]) && !
double.IsInfinity(siteCoordsY[i]),
1493 $
"Potential error in level generation: invalid voronoi site ({siteCoordsY[i]})");
1496 $
"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
1498 siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height,
1499 $
"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
1504 "Potential error in level generation: two voronoi sites are extremely close to each other.");
1509 private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel,
float variance)
1511 List<Point> pathNodes =
new List<Point> { startPosition };
1515 for (
int x = startPosition.X + nodeInterval.X;
1516 x < endPosition.X - nodeInterval.X;
1517 x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient))
1519 Point nodePos =
new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient));
1523 if (pathNodes.Count > 2 || parentTunnel !=
null)
1525 nodePos.Y = (int)MathHelper.Clamp(
1527 pathNodes.Last().Y - pathBorders.Height * variance * 0.5f,
1528 pathNodes.Last().Y + pathBorders.Height * variance * 0.5f);
1530 if (pathNodes.Count == 1)
1534 nodePos.Y = (int)(startPosition.Y + Math.Abs(nodePos.Y - startPosition.Y) * -Math.Sign(nodePos.Y - pathBorders.Center.Y));
1535 nodePos.Y = MathHelper.Clamp(nodePos.Y, pathBorders.Y, pathBorders.Bottom);
1539 foreach (Tunnel tunnel
in Tunnels)
1541 for (
int i = 1; i < tunnel.Nodes.Count; i++)
1543 Point node1 = tunnel.Nodes[i - 1];
1544 Point node2 = tunnel.Nodes[i];
1545 if (node1.X >= nodePos.X) {
continue; }
1546 if (node2.X <= pathNodes.Last().X) {
continue; }
1547 if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) {
continue; }
1548 if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth &&
1549 !MathUtils.LineSegmentsIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2()))
1554 if (nodePos.Y < pathNodes.Last().Y)
1556 nodePos.Y = Math.Min(Math.Max(node1.Y, node2.Y) + tunnel.MinWidth * 2, pathBorders.Bottom);
1560 nodePos.Y = Math.Max(Math.Min(node1.Y, node2.Y) - tunnel.MinWidth * 2, pathBorders.Y);
1566 pathNodes.Add(nodePos);
1569 if (pathNodes.Count == 1)
1571 pathNodes.Add(
new Point(pathBorders.Center.X, pathBorders.Y));
1574 pathNodes.Add(endPosition);
1578 private List<VoronoiCell> CreateHoles(
float holeProbability, Rectangle limits,
int submarineSize)
1580 List<VoronoiCell> toBeRemoved =
new List<VoronoiCell>();
1583 if (cell.
Edges.Any(e => e.NextToCave)) {
continue; }
1584 if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) {
continue; }
1587 float closestDist = 0.0f;
1588 Point? closestTunnelNode =
null;
1589 foreach (Tunnel tunnel
in Tunnels)
1591 foreach (Point node
in tunnel.Nodes)
1593 float dist = Math.Abs(cell.
Center.X - node.X);
1594 if (closestTunnelNode ==
null || dist < closestDist)
1597 closestTunnelNode = node;
1602 if (closestTunnelNode !=
null && closestTunnelNode.Value.Y < cell.
Center.Y) {
continue; }
1604 toBeRemoved.Add(cell);
1610 private void EnlargePath(List<VoronoiCell> pathCells,
float minWidth)
1612 if (minWidth <= 0.0f) {
return; }
1614 List<VoronoiCell> removedCells = GetTooCloseCells(pathCells, minWidth);
1619 pathCells.Add(removedCell);
1624 private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel)
1626 if (tunnel.Cells.Count == 0) {
return; }
1628 List<WayPoint> wayPoints =
new List<WayPoint>();
1629 WayPoint prevWayPoint =
null;
1630 for (
int i = 0; i < tunnel.Cells.Count; i++)
1632 tunnel.Cells[i].CellType =
CellType.Path;
1633 var newWaypoint =
new WayPoint(
new Rectangle((
int)tunnel.Cells[i].Site.Coord.X, (
int)tunnel.Cells[i].Center.Y, 10, 10),
null)
1637 wayPoints.Add(newWaypoint);
1639 if (prevWayPoint !=
null)
1641 bool solidCellBetween =
false;
1642 foreach (
GraphEdge edge
in tunnel.Cells[i].Edges)
1645 MathUtils.LineSegmentsIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.
Point1, edge.Point2))
1647 solidCellBetween =
true;
1652 if (solidCellBetween)
1656 var edgeBetweenCells = tunnel.Cells[i].Edges.Find(e => e.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[i - 1]);
1657 if (edgeBetweenCells !=
null)
1659 var edgeWaypoint =
new WayPoint(
new Rectangle((
int)edgeBetweenCells.Center.X, (
int)edgeBetweenCells.Center.Y, 10, 10),
null)
1663 prevWayPoint.ConnectTo(edgeWaypoint);
1664 prevWayPoint = edgeWaypoint;
1667 prevWayPoint.ConnectTo(newWaypoint);
1672 for (
int j = i - 2; j > 0 && j > i - 5; j--)
1674 foreach (
GraphEdge edge
in tunnel.Cells[i].Edges)
1676 if (Vector2.DistanceSquared(edge.
Point1, edge.Point2) < 30.0f * 30.0f) {
continue; }
1679 var edgeWaypoint =
new WayPoint(
new Rectangle((
int)edge.
Center.X, (
int)edge.
Center.Y, 10, 10),
null)
1683 wayPoints[j].ConnectTo(edgeWaypoint);
1684 edgeWaypoint.ConnectTo(newWaypoint);
1690 prevWayPoint = newWaypoint;
1693 tunnel.WayPoints.AddRange(wayPoints);
1696 if (parentTunnel !=
null)
1698 var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel);
1699 if (parentStart !=
null)
1701 wayPoints.First().ConnectTo(parentStart);
1705 var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel);
1706 if (parentEnd !=
null)
1708 wayPoints.Last().ConnectTo(parentEnd);
1714 private void ConnectWaypoints(Tunnel tunnel, Tunnel parentTunnel)
1716 foreach (WayPoint wayPoint
in tunnel.WayPoints)
1718 var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel);
1719 if (closestWaypoint ==
null) {
continue; }
1721 ConvertUnits.ToSimUnits(wayPoint.WorldPosition),
1722 ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) ==
null)
1725 ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel);
1730 private List<WayPoint> ConnectWaypoints(WayPoint wp1, WayPoint wp2,
float interval)
1732 List<WayPoint> newWaypoints =
new List<WayPoint>();
1734 Vector2 diff = wp2.WorldPosition - wp1.WorldPosition;
1735 float dist = diff.Length();
1737 WayPoint prevWaypoint = wp1;
1738 for (
float x = interval; x < dist - interval; x += interval)
1740 var newWaypoint =
new WayPoint(wp1.WorldPosition + (diff / dist * x),
SpawnType.Path, submarine:
null);
1741 prevWaypoint.ConnectTo(newWaypoint);
1742 prevWaypoint = newWaypoint;
1743 newWaypoints.Add(newWaypoint);
1745 prevWaypoint.ConnectTo(wp2);
1747 return newWaypoints;
1750 private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel)
1752 return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints);
1755 private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable<WayPoint> waypoints, Func<WayPoint, bool> filter =
null)
1757 float closestDist =
float.PositiveInfinity;
1758 WayPoint closestWayPoint =
null;
1759 foreach (WayPoint otherWayPoint
in waypoints)
1761 float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition);
1762 if (dist < closestDist)
1766 if (!filter(otherWayPoint)) {
continue; }
1769 closestWayPoint = otherWayPoint;
1772 return closestWayPoint;
1775 private List<VoronoiCell> GetTooCloseCells(List<VoronoiCell> emptyCells,
float minDistance)
1777 List<VoronoiCell> tooCloseCells =
new List<VoronoiCell>();
1778 if (minDistance <= 0.0f) {
return tooCloseCells; }
1779 foreach (var cell
in emptyCells.Distinct())
1781 foreach (var tooCloseCell
in GetTooCloseCells(cell.Center, minDistance))
1783 if (!tooCloseCells.Contains(tooCloseCell))
1785 tooCloseCells.Add(tooCloseCell);
1789 return tooCloseCells;
1794 HashSet<VoronoiCell> tooCloseCells =
new HashSet<VoronoiCell>();
1795 var closeCells =
GetCells(position, searchDepth: Math.Max((
int)Math.Ceiling(minDistance /
GridCellSize), 3));
1796 float minDistSqr = minDistance * minDistance;
1799 bool tooClose =
false;
1811 if (Vector2.DistanceSquared(edge.
Point1, position) < minDistSqr ||
1812 Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
1813 MathUtils.LineSegmentToPointDistanceSquared(edge.
Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
1820 if (tooClose) { tooCloseCells.Add(cell); }
1822 return tooCloseCells.ToList();
1825 private void GenerateAbyssPositions()
1828 for (
int i = 0; i < count; i++)
1830 float xPos = MathHelper.Lerp(borders.X, borders.Right, i / (
float)(count - 1));
1836 float yPos = MathHelper.Lerp(
AbyssStart, Math.Max(seaFloorPos,
AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient));
1840 if (abyssIsland.Area.Contains(
new Point((
int)xPos, (
int)yPos)))
1842 xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
1846 PositionsOfInterest.Add(
new InterestingPosition(
new Point((
int)xPos, (
int)yPos), PositionType.Abyss));
1850 private void GenerateAbyssArea()
1852 int abyssStartY = borders.Y - 5000;
1853 int abyssEndY = Math.Max(abyssStartY - 100000,
BottomPos + 1000);
1854 int abyssHeight = abyssStartY - abyssEndY;
1856 if (abyssHeight < 0)
1858 abyssStartY = borders.Y;
1860 if (abyssStartY - abyssEndY < 1000)
1863 DebugConsole.ThrowError(
"Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
1865 DebugConsole.AddWarning(
"Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
1869 else if (abyssHeight > 30000)
1873 if (abyssEndY + CrushDepth < 0 && abyssStartY > -
CrushDepth)
1875 abyssEndY += Math.Min(-(abyssEndY + (
int)
CrushDepth), abyssHeight / 2);
1878 if (abyssStartY - abyssEndY < 10000)
1880 abyssStartY = borders.Y;
1884 AbyssArea =
new Rectangle(borders.X, abyssEndY, borders.Width, abyssStartY - abyssEndY);
1887 private void GenerateAbyssGeometry()
1892 Point siteInterval =
new Point(500, 500);
1893 Point siteVariance =
new Point(200, 200);
1895 Point islandSize = Vector2.Lerp(
1898 Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint();
1900 if (
AbyssArea.Height < islandSize.Y) {
return; }
1902 int createdCaves = 0;
1904 for (
int i = 0; i < islandCount; i++)
1906 Point islandPosition = Point.Zero;
1911 const int MaxTries = 20;
1914 islandPosition =
new Point(
1915 Rand.Range(
AbyssArea.X,
AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient),
1916 Rand.Range(
AbyssArea.Y,
AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient));
1919 islandPosition.Y = Math.Max(islandPosition.Y, (
int)
GetBottomPosition(islandPosition.X).Y + 500);
1920 islandPosition.Y = Math.Max(islandPosition.Y, (
int)
GetBottomPosition(islandPosition.X + islandArea.Width).Y + 500);
1922 islandArea.Location = islandPosition;
1925 }
while ((
AbyssIslands.Any(island => island.Area.Intersects(islandArea)) || islandArea.Bottom >
AbyssArea.Bottom) && tries < MaxTries);
1927 if (tries >= MaxTries)
1934 (i == islandCount - 1 && createdCaves == 0) ||
1939 float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f;
1940 var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (
int)(radiusVariance * 2), islandArea.Height - (
int)(radiusVariance * 2), 16, radiusVariance: radiusVariance);
1941 Vector2 position = islandArea.Center.ToVector2();
1942 for (
int j = 0; j < vertices.Count; j++)
1944 vertices[j] += position;
1947 AbyssIslands.Add(
new AbyssIsland(islandArea, newChunk.Cells));
1951 var
siteCoordsX =
new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
1952 var siteCoordsY =
new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
1954 for (
int x = islandArea.X; x < islandArea.Right; x += siteInterval.X)
1956 for (
int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y)
1958 siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient));
1959 siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient));
1964 var islandCells = CaveGenerator.GraphEdgesToCells(graphEdges, islandArea,
GridCellSize, out var cellGrid);
1967 for (
int j = islandCells.Count - 1; j >= 0; j--)
1969 var cell = islandCells[j];
1970 double xDiff = (cell.Site.Coord.X - islandArea.Center.X) / (islandArea.Width * 0.5);
1971 double yDiff = (cell.Site.Coord.Y - islandArea.Center.Y) / (islandArea.Height * 0.5);
1974 if (yDiff < 0) { xDiff += xDiff * Math.Abs(yDiff); }
1976 double normalizedDist = Math.Sqrt(xDiff * xDiff + yDiff * yDiff);
1977 if (normalizedDist > 0.95 ||
1978 cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.X)) ||
1979 cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.Right)) ||
1980 cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Y)) ||
1981 cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Bottom)))
1984 islandCells.RemoveAt(j);
1988 var caveParams = CaveGenerationParams.GetRandom(
this, abyss:
true, rand: Rand.RandSync.ServerAndClient);
1990 float caveScaleRelativeToIsland = 0.7f;
1993 new Point(islandArea.Center.X, islandArea.Center.Y + (
int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2),
1994 new Point((
int)(islandArea.Size.X * caveScaleRelativeToIsland), (
int)(islandArea.Size.Y * caveScaleRelativeToIsland)));
1995 AbyssIslands.Add(
new AbyssIsland(islandArea, islandCells));
2000 private void GenerateSeaFloorPositions()
2005 bottomPositions =
new List<Point>
2011 for (
int i = 0; i < mountainCount; i++)
2013 bottomPositions.Add(
2014 new Point(
Size.X / (mountainCount + 1) * (i + 1),
2019 int minVertexInterval = 5000;
2020 float currInverval =
Size.X / 2;
2021 while (currInverval > minVertexInterval)
2023 for (
int i = 0; i < bottomPositions.Count - 1; i++)
2025 bottomPositions.Insert(i + 1,
2027 (bottomPositions[i].X + bottomPositions[i + 1].X) / 2,
2038 private void GenerateSeaFloor()
2044 ConvertUnits.ToSimUnits(
new Vector2(borders.X, 0)),
2045 ConvertUnits.ToSimUnits(
new Vector2(borders.Right, 0)));
2055 private void GenerateCaves(Tunnel parentTunnel)
2059 var caveParams = CaveGenerationParams.GetRandom(
this, abyss:
false, rand: Rand.RandSync.ServerAndClient);
2060 Point caveSize =
new Point(
2061 Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient),
2062 Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient));
2063 int padding = (int)(caveSize.X * 1.2f);
2066 int radius = Math.Max(caveSize.X, caveSize.Y) / 2;
2067 var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.25f, asCloseAsPossible:
true, allowedArea);
2069 GenerateCave(caveParams, parentTunnel, cavePos, caveSize);
2071 CalculateTunnelDistanceField(
null);
2075 private void GenerateCave(CaveGenerationParams caveParams, Tunnel parentTunnel, Point cavePos, Point caveSize)
2077 Rectangle caveArea =
new Rectangle(cavePos -
new Point(caveSize.X / 2, caveSize.Y / 2), caveSize);
2078 Point closestParentNode = parentTunnel.Nodes.First();
2079 double closestDist =
double.PositiveInfinity;
2080 foreach (Point node
in parentTunnel.Nodes)
2082 if (caveArea.Contains(node)) {
continue; }
2083 double dist = MathUtils.DistanceSquared((
double)node.X, (
double)node.Y, (
double)cavePos.X, (
double)cavePos.Y);
2084 if (dist < closestDist)
2086 closestParentNode = node;
2091 if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(),
new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
2093 caveStartPosVector = caveArea.Location.ToVector2();
2096 Point caveStartPos = caveStartPosVector.ToPoint();
2097 Point caveEndPos = cavePos - (caveStartPos - cavePos);
2099 Cave cave =
new Cave(caveParams, caveArea, caveStartPos, caveEndPos);
2102 var caveSegments = MathUtils.GenerateJaggedLine(
2103 caveStartPos.ToVector2(), caveEndPos.ToVector2(),
2105 offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f,
2107 rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
2109 if (!caveSegments.Any()) {
return; }
2111 List<Tunnel> caveBranches =
new List<Tunnel>();
2113 var tunnel =
new Tunnel(
TunnelType.Cave, SegmentsToNodes(caveSegments), 150, parentTunnel);
2115 caveBranches.Add(tunnel);
2117 int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient);
2118 for (
int j = 0; j < branches; j++)
2120 Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient);
2121 Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2();
2122 Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2();
2123 var branchSegments = MathUtils.GenerateJaggedLine(
2124 branchStartPos, branchEndPos,
2126 offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f,
2128 rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
2129 if (!branchSegments.Any()) {
continue; }
2131 var branch =
new Tunnel(
TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch);
2133 caveBranches.Add(branch);
2136 foreach (Tunnel branch
in caveBranches)
2138 var node = branch.Nodes.Last();
2140 cave.Tunnels.Add(branch);
2143 static List<Point> SegmentsToNodes(List<Vector2[]> segments)
2145 List<Point> nodes =
new List<Point>();
2146 foreach (Vector2[] segment
in segments)
2148 nodes.Add(segment[0].ToPoint());
2150 nodes.Add(segments.Last()[1].ToPoint());
2155 private void GenerateRuin(Point ruinPos,
bool mirror,
bool requireMissionReadyRuin)
2161 float weight = MathUtils.Pow(1 - diff, 10);
2162 return Math.Max(weight, 0);
2165 if (requireMissionReadyRuin)
2167 possibleRuinGenerationParams = possibleRuinGenerationParams.Where(p => p.
IsMissionReady);
2169 if (possibleRuinGenerationParams.Multiple())
2173 possibleRuinGenerationParams = possibleRuinGenerationParams
2176 .OrderByDescending(GetWeight)
2177 .Take((
int)Math.Max(Math.Round(possibleRuinGenerationParams.Count() / 4f), 1));
2179 var selectedRuinGenerationParams = possibleRuinGenerationParams.GetRandomByWeight(GetWeight, randSync: Rand.RandSync.ServerAndClient);
2180 if (selectedRuinGenerationParams ==
null)
2182 DebugConsole.ThrowError(
"Failed to generate alien ruins. Could not find any RuinGenerationParameters!");
2185 DebugConsole.NewMessage($
"Creating alien ruins using {selectedRuinGenerationParams.Identifier} (preferred difficulty: {selectedRuinGenerationParams.PreferredDifficulty}, current difficulty {Difficulty})", color: Color.Yellow, debugOnly:
true);
2188 if (locationType ==
null)
2190 locationType = LocationType.
Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
2191 if (selectedRuinGenerationParams.AllowedLocationTypes.Any())
2193 locationType = LocationType.Prefabs.Where(lt =>
2194 selectedRuinGenerationParams.AllowedLocationTypes.Any(allowedType =>
2195 allowedType ==
"any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient);
2199 var ruin =
new Ruin(
this, selectedRuinGenerationParams, locationType, ruinPos, mirror);
2200 if (ruin.Submarine !=
null)
2202 SetLinkedSubCrushDepth(ruin.Submarine);
2206 var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4);
2211 if (
ExtraWalls.Any(w => w.Cells.Contains(cell))) {
continue; }
2214 if (ruin.Area.Contains(e.
Point1) || ruin.Area.Contains(e.Point2) ||
2215 MathUtils.GetLineRectangleIntersection(e.
Point1, e.Point2, ruin.Area, out _))
2218 for (
int x = 0; x < cellGrid.GetLength(0); x++)
2220 for (
int y = 0; y < cellGrid.GetLength(1); y++)
2222 cellGrid[x, y].Remove(cell);
2231 ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center);
2234 private void GenerateRuinWayPoints(
Ruin ruin)
2236 var tooClose = GetTooCloseCells(ruin.
Area.Center.ToVector2(), Math.Max(ruin.
Area.Width, ruin.
Area.Height) * 6);
2238 List<WayPoint> wayPoints =
new List<WayPoint>();
2239 float outSideWaypointInterval = 500.0f;
2240 WayPoint[,] cornerWaypoint =
new WayPoint[2, 2];
2242 waypointArea.Inflate(100, 100);
2245 for (
int i = 0; i < 2; i++)
2247 for (
float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval)
2249 var wayPoint =
new WayPoint(
new Vector2(x, waypointArea.Y + waypointArea.Height * i),
SpawnType.Path,
null)
2253 wayPoints.Add(wayPoint);
2254 if (x == waypointArea.X + outSideWaypointInterval)
2256 cornerWaypoint[i, 0] = wayPoint;
2260 wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
2263 cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1];
2266 for (
int i = 0; i < 2; i++)
2268 WayPoint wayPoint =
null;
2269 for (
float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval)
2271 wayPoint =
new WayPoint(
new Vector2(waypointArea.X + waypointArea.Width * i, y),
SpawnType.Path,
null)
2275 wayPoints.Add(wayPoint);
2276 if (y == waypointArea.Y)
2278 wayPoint.ConnectTo(cornerWaypoint[0, i]);
2282 wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
2285 wayPoint.ConnectTo(cornerWaypoint[1, i]);
2289 for (
int i = wayPoints.Count - 1; i >= 0; i--)
2291 WayPoint wp = wayPoints[i];
2292 var overlappingCell = tooClose.Find(c => c.CellType !=
CellType.Removed && c.IsPointInside(wp.WorldPosition));
2293 if (overlappingCell ==
null) {
continue; }
2294 if (wp.linkedTo.Count > 1)
2296 WayPoint linked1 = wp.linkedTo[0] as WayPoint;
2297 WayPoint linked2 = wp.linkedTo[1] as WayPoint;
2298 linked1.ConnectTo(linked2);
2301 wayPoints.RemoveAt(i);
2304 Debug.Assert(wayPoints.Any(),
"Couldn't generate waypoints around ruins.");
2307 foreach (Gap g
in Gap.GapList)
2309 if (g.Submarine != ruin.
Submarine || g.IsRoomToRoom || g.linkedTo.Count == 0) {
continue; }
2310 var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g);
2311 if (gapWaypoint ==
null) {
continue; }
2314 Vector2 entranceDir = Vector2.Zero;
2317 entranceDir = Vector2.UnitX * 2 * Math.Sign(g.WorldPosition.X - g.linkedTo[0].WorldPosition.X);
2321 entranceDir = Vector2.UnitY * 2 * Math.Sign(g.WorldPosition.Y - g.linkedTo[0].WorldPosition.Y);
2323 var entranceWayPoint =
new WayPoint(g.WorldPosition + entranceDir * 64.0f,
SpawnType.Path,
null)
2327 entranceWayPoint.ConnectTo(gapWaypoint);
2328 var closestWp = FindClosestWayPoint(entranceWayPoint.WorldPosition, wayPoints, (wp) =>
2330 return Submarine.PickBody(
2331 ConvertUnits.ToSimUnits(wp.WorldPosition),
2332 ConvertUnits.ToSimUnits(entranceWayPoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
2334 if (closestWp ==
null) {
continue; }
2335 ConnectWaypoints(entranceWayPoint, closestWp, outSideWaypointInterval);
2339 WayPoint prevWp = FindClosestWayPoint(ruin.
PathCells.First().Center, wayPoints, (wp) =>
2341 return Submarine.PickBody(
2342 ConvertUnits.ToSimUnits(wp.WorldPosition),
2343 ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
2347 for (
int i = 0; i < ruin.
PathCells.Count; i++)
2350 if (connectingEdge !=
null)
2352 var edgeWayPoint =
new WayPoint(connectingEdge.Center,
SpawnType.Path, submarine:
null);
2353 ConnectWaypoints(prevWp, edgeWayPoint, outSideWaypointInterval);
2354 prevWp = edgeWayPoint;
2356 var newWaypoint =
new WayPoint(ruin.
PathCells[i].Center,
SpawnType.Path, submarine:
null);
2357 ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval);
2358 prevWp = newWaypoint;
2360 var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition,
Tunnels.SelectMany(t => t.WayPoints));
2361 ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval);
2365 private Point FindPosAwayFromMainPath(
double minDistance,
bool asCloseAsPossible, Rectangle? limits =
null)
2368 if (pointsAboveBottom.Count == 0)
2370 DebugConsole.ThrowError(
"Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?");
2374 var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits ==
null || limits.Value.Contains(d.point)));
2375 if (!validPoints.Any())
2377 DebugConsole.AddWarning(
"Failed to find a valid position far enough from the main path. Choosing the furthest possible position.\n" + Environment.StackTrace);
2381 validPoints = pointsAboveBottom.FindAll(d => limits.Value.Contains(d.point));
2383 if (!validPoints.Any())
2386 validPoints = pointsAboveBottom;
2388 (Point position,
double distance) furthestPoint = validPoints.First();
2389 foreach (var point
in validPoints)
2391 if (point.distance > furthestPoint.distance)
2393 furthestPoint = point;
2396 return furthestPoint.position;
2399 if (asCloseAsPossible)
2402 (Point position,
double distance) closestPoint = validPoints.First();
2403 foreach (var point
in validPoints)
2405 if (point.distance < closestPoint.distance)
2407 closestPoint = point;
2410 return closestPoint.position;
2414 return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point;
2418 private void CalculateTunnelDistanceField(List<Point> ruinPositions)
2425 for (
int x =
Size.X - 1; x >= 0; x -= density)
2427 for (
int y = 0; y <
Size.Y; y += density)
2435 for (
int x = 0; x <
Size.X; x += density)
2437 for (
int y = 0; y <
Size.Y; y += density)
2444 void addPoint(
int x,
int y)
2446 Point point =
new Point(x, y);
2447 double shortestDistSqr =
double.PositiveInfinity;
2448 foreach (Tunnel tunnel
in Tunnels)
2450 for (
int i = 1; i < tunnel.Nodes.Count; i++)
2452 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
2455 if (ruinPositions !=
null)
2457 int ruinSize = 10000;
2458 foreach (Point ruinPos
in ruinPositions)
2460 double xDiff = Math.Abs(point.X - ruinPos.X);
2461 double yDiff = Math.Abs(point.Y - ruinPos.Y);
2462 if (xDiff < ruinSize && yDiff < ruinSize)
2464 shortestDistSqr = 0.0f;
2468 shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr);
2472 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((
double)point.X, (
double)point.Y, (
double)startPosition.X, (
double)startPosition.Y));
2473 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((
double)point.X, (
double)point.Y, (
double)startExitPosition.X, (
double)borders.Bottom));
2474 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((
double)point.X, (
double)point.Y, (
double)endPosition.X, (
double)endPosition.Y));
2475 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((
double)point.X, (
double)point.Y, (
double)endExitPosition.X, (
double)borders.Bottom));
2480 private double GetDistToTunnel(Vector2 position, Tunnel tunnel)
2482 Point point = position.ToPoint();
2483 double shortestDistSqr =
double.PositiveInfinity;
2484 for (
int i = 1; i < tunnel.Nodes.Count; i++)
2486 shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
2488 return Math.Sqrt(shortestDistSqr);
2491 private DestructibleLevelWall CreateIceChunk(IEnumerable<GraphEdge> edges, Vector2 position,
float? health =
null)
2493 List<Vector2> vertices =
new List<Vector2>();
2496 if (!vertices.Any())
2498 vertices.Add(edge.
Point1);
2500 else if (!vertices.Any(v => v.NearlyEquals(edge.
Point1)))
2502 vertices.Add(edge.
Point1);
2504 else if (!vertices.Any(v => v.NearlyEquals(edge.Point2)))
2506 vertices.Add(edge.Point2);
2509 if (vertices.Count < 3) {
return null; }
2510 return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health);
2513 private DestructibleLevelWall CreateIceChunk(List<Vector2> vertices, Vector2 position,
float? health =
null)
2515 DestructibleLevelWall newChunk =
new DestructibleLevelWall(vertices, Color.White,
this, health,
true);
2516 newChunk.Body.Position = ConvertUnits.ToSimUnits(position);
2517 newChunk.Cells.ForEach(c => c.Translation = position);
2518 newChunk.Body.BodyType = BodyType.Dynamic;
2519 newChunk.Body.FixedRotation =
true;
2520 newChunk.Body.LinearDamping = 0.5f;
2521 newChunk.Body.IgnoreGravity =
true;
2522 newChunk.Body.Mass *= 10.0f;
2526 private DestructibleLevelWall CreateIceSpire(List<GraphEdge> usedSpireEdges)
2528 const float maxLength = 15000.0f;
2529 float minEdgeLength = 100.0f;
2530 var mainPathPos =
PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient);
2531 double closestDistSqr =
double.PositiveInfinity;
2539 if (!edge.
IsSolid || usedSpireEdges.Contains(edge) || edge.
NextToCave) {
continue; }
2542 if (Vector2.DistanceSquared(edge.
Center,
StartPosition) < maxLength * maxLength) {
continue; }
2543 if (Vector2.DistanceSquared(edge.
Center,
EndPosition) < maxLength * maxLength) {
continue; }
2545 float edgeLengthSqr = Vector2.DistanceSquared(edge.
Point1, edge.Point2);
2546 if (edgeLengthSqr > 1000.0f * 1000.0f || edgeLengthSqr < minEdgeLength * minEdgeLength) {
continue; }
2548 if (Vector2.Dot(Vector2.Normalize(mainPathPos.Position.ToVector2()) - edge.
Center, edge.
GetNormal(cell)) < 0.5f) {
continue; }
2549 double distSqr = MathUtils.DistanceSquared(edge.
Center.X, edge.
Center.Y, mainPathPos.Position.X, mainPathPos.Position.Y);
2550 if (distSqr < closestDistSqr)
2552 closestDistSqr = distSqr;
2559 if (closestEdge ==
null) {
return null; }
2561 usedSpireEdges.Add(closestEdge);
2563 Vector2 edgeNormal = closestEdge.
GetNormal(closestCell);
2564 float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength);
2565 spireLength *= MathHelper.Lerp(0.3f, 1.5f,
Difficulty / 100.0f);
2567 Vector2 extrudedPoint1 = closestEdge.
Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
2568 Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
2569 List<Vector2> vertices =
new List<Vector2>()
2572 extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
2573 extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
2576 Vector2 center = Vector2.Zero;
2577 vertices.ForEach(v => center += v);
2578 center /= vertices.Count;
2579 DestructibleLevelWall spire =
new DestructibleLevelWall(vertices.Select(v => v - center).ToList(), Color.White,
this, health: 100.0f, giftWrap:
true);
2582 foreach (
GraphEdge edge
in spire.Cells[0].Edges)
2584 if ((edge.
Point1.NearlyEquals(closestEdge.
Point1 - center) && edge.Point2.NearlyEquals(closestEdge.Point2 - center)) ||
2585 (edge.
Point1.NearlyEquals(closestEdge.Point2 - center) && edge.Point2.NearlyEquals(closestEdge.
Point1 - center)))
2591 spire.GenerateVertices();
2593 spire.Body.Position = ConvertUnits.ToSimUnits(center);
2594 spire.Body.BodyType = BodyType.Static;
2595 spire.Body.FixedRotation =
true;
2596 spire.Body.IgnoreGravity =
true;
2597 spire.Body.Mass *= 10.0f;
2598 spire.Cells.ForEach(c => c.Translation = center);
2599 spire.WallDamageOnTouch = 50.0f;
2604 private static int nextPathPointId;
2605 public List<PathPoint>
PathPoints {
get; } =
new List<PathPoint>();
2608 public string Id {
get; }
2629 private PathPoint(
string id, Vector2 position,
bool shouldContainResources,
TunnelType tunnelType, List<Identifier> resourceTags, List<Identifier> resourceIds, List<ClusterLocation> clusterLocations)
2641 : this(id, position, shouldContainResources, tunnelType, new List<Identifier>(), new List<Identifier>(), new List<
ClusterLocation>())
2669 Resources = initializeResourceList ?
new List<Item>() :
null;
2673 Cell == anotherLocation.Cell &&
Edge == anotherLocation.
Edge;
2686 private void GenerateItems()
2690 Vector2 commonnessRange =
new Vector2(
float.MaxValue,
float.MinValue), caveCommonnessRange =
new Vector2(
float.MaxValue,
float.MinValue);
2693 if (itemPrefab.GetCommonnessInfo(
this) is { CanAppear: true } commonnessInfo)
2695 if (commonnessInfo.Commonness > 0.0)
2697 if (commonnessInfo.Commonness < commonnessRange.X) { commonnessRange.X = commonnessInfo.Commonness; }
2698 if (commonnessInfo.Commonness > commonnessRange.Y) { commonnessRange.Y = commonnessInfo.Commonness; }
2700 if (commonnessInfo.CaveCommonness > 0.0)
2702 if (commonnessInfo.CaveCommonness < caveCommonnessRange.X) { caveCommonnessRange.X = commonnessInfo.CaveCommonness; }
2703 if (commonnessInfo.CaveCommonness > caveCommonnessRange.Y) { caveCommonnessRange.Y = commonnessInfo.CaveCommonness; }
2705 levelResources.Add((itemPrefab, commonnessInfo));
2709 itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo))
2711 fixedResources.Add((itemPrefab, fixedQuantityResourceInfo));
2715 DebugConsole.Log(
"Generating level resources...");
2716 var allValidLocations = GetAllValidClusterLocations();
2717 var maxResourceOverlap = 0.4f;
2719 foreach (var (itemPrefab, resourceInfo) in fixedResources)
2721 for (
int i = 0; i < resourceInfo.ClusterQuantity; i++)
2723 var location = allValidLocations.GetRandom(l =>
2725 if (l.Cell ==
null || l.Edge ==
null) { return false; }
2726 if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
2727 if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X <
Size.X * 0.25f) { return false; }
2728 if (l.EdgeCenter.Y <
AbyssArea.Bottom) { return false; }
2729 return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
2731 }, randSync: Rand.RandSync.ServerAndClient);
2733 if (location.Cell ==
null || location.Edge ==
null) {
break; }
2735 PlaceResources(itemPrefab, resourceInfo.ClusterSize, location, out _);
2736 var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
2737 allValidLocations.RemoveAt(locationIndex);
2744 var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f);
2745 if (abyssResourcePrefabs.Any())
2748 for (
int i = 0; i < abyssClusterCount; i++)
2750 var selectedPrefab = ToolBox.SelectWeightedRandom(
2751 abyssResourcePrefabs.Select(r => r.itemPrefab).ToList(),
2752 abyssResourcePrefabs.Select(r => r.commonnessInfo.AbyssCommonness).ToList(),
2753 Rand.RandSync.ServerAndClient);
2755 var location = allValidLocations.GetRandom(l =>
2757 if (l.Cell ==
null || l.Edge ==
null) {
return false; }
2758 if (l.EdgeCenter.Y >
AbyssArea.Bottom) {
return false; }
2759 l.InitializeResources();
2760 return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _);
2761 }, randSync: Rand.RandSync.ServerAndClient);
2763 if (location.Cell ==
null || location.Edge ==
null) {
break; }
2766 PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0);
2767 var abyssClusterLocation =
new ClusterLocation(location.Cell, location.Edge, initializeResourceList:
true);
2768 abyssClusterLocation.Resources.AddRange(placedResources);
2771 var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
2772 allValidLocations.RemoveAt(locationIndex);
2778 nextPathPointId = 0;
2780 foreach (Tunnel tunnel
in Tunnels)
2782 var tunnelLength = 0.0f;
2783 for (
int i = 1; i < tunnel.Nodes.Count; i++)
2785 tunnelLength += Vector2.Distance(tunnel.Nodes[i - 1].ToVector2(), tunnel.Nodes[i].ToVector2());
2788 var nextNodeIndex = 1;
2789 var positionOnPath = tunnel.Nodes.First().ToVector2();
2790 var lastNodePos = tunnel.Nodes.Last().ToVector2();
2791 var reachedLastNode =
false;
2795 var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient);
2796 reachedLastNode = !CalculatePositionOnPath();
2797 var
id =
Tunnels.IndexOf(tunnel) +
":" + nextPathPointId++;
2798 var spawnChance = tunnel.Type ==
TunnelType.Cave || tunnel.ParentTunnel?.Type ==
TunnelType.Cave ?
2800 var containsResources =
true;
2801 if (spawnChance < 1.0f)
2803 var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient);
2804 containsResources = spawnPointRoll <= spawnChance;
2806 var tunnelType = tunnel.Type;
2807 if (tunnel.ParentTunnel !=
null && tunnel.ParentTunnel.Type ==
TunnelType.Cave) { tunnelType =
TunnelType.Cave; }
2808 PathPoints.Add(
new PathPoint(
id, positionOnPath, containsResources, tunnel.Type));
2810 bool CalculatePositionOnPath(
float checkedDist = 0.0f)
2812 if (nextNodeIndex >= tunnel.Nodes.Count) {
return false; }
2813 var distToNextNode = Vector2.Distance(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2());
2814 var lerpAmount = (distance - checkedDist) / distToNextNode;
2815 if (lerpAmount <= 1.0f)
2817 positionOnPath = Vector2.Lerp(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2(), lerpAmount);
2822 positionOnPath = tunnel.Nodes[nextNodeIndex++].ToVector2();
2823 return CalculatePositionOnPath(checkedDist + distToNextNode);
2826 }
while (!reachedLastNode && Vector2.DistanceSquared(positionOnPath, lastNodePos) > (intervalRange.Y * intervalRange.Y));
2830 Identifier[] exclusiveResourceTags =
new Identifier[2] {
"ore".ToIdentifier(),
"plant".ToIdentifier() };
2832 var disabledPathPoints =
new List<string>();
2837 if (!pathPoint.ShouldContainResources) {
continue; }
2838 GenerateFirstCluster(pathPoint);
2839 if (pathPoint.ClusterLocations.Count > 0) {
continue; }
2840 disabledPathPoints.Add(pathPoint.Id);
2843 foreach (
string pathPointId
in disabledPathPoints)
2845 if (
PathPoints.FirstOrNull(p => p.Id == pathPointId) is PathPoint pathPoint)
2847 PathPoints.RemoveAll(p => p.Id == pathPointId);
2848 PathPoints.Add(pathPoint.WithResources(
false));
2852 var excludedPathPointIds =
new List<string>();
2855 var availablePathPoints =
PathPoints.Where(p =>
2856 p.ShouldContainResources && p.NextClusterProbability > 0 &&
2857 !excludedPathPointIds.Contains(p.Id)).ToList();
2859 if (availablePathPoints.None()) {
break; }
2861 var pathPoint = ToolBox.SelectWeightedRandom(
2862 availablePathPoints,
2863 availablePathPoints.Select(p => p.NextClusterProbability).ToList(),
2864 Rand.RandSync.ServerAndClient);
2866 GenerateAdditionalCluster(pathPoint);
2870 int spawnPointsContainingResources =
PathPoints.Where(p => p.ClusterLocations.Any()).Count();
2871 string percentage =
string.Format(CultureInfo.InvariantCulture,
"{0:P2}", (
float)spawnPointsContainingResources /
PathPoints.Count);
2872 DebugConsole.NewMessage($
"Level resources spawned: {itemCount}\n" +
2873 $
" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" +
2874 $
" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk");
2877 DebugConsole.NewMessage($
"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" +
2878 $
" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk");
2882 DebugConsole.Log(
"Level resources generated");
2884 bool GenerateFirstCluster(PathPoint pathPoint)
2886 var intervalRange = pathPoint.TunnelType !=
TunnelType.Cave ?
2888 allValidLocations.Sort((x, y) =>
2889 Vector2.DistanceSquared(pathPoint.Position, x.EdgeCenter)
2890 .CompareTo(Vector2.DistanceSquared(pathPoint.Position, y.EdgeCenter)));
2891 var selectedLocationIndex = -1;
2892 var generatedCluster =
false;
2893 for (
int i = 0; i < allValidLocations.Count; i++)
2895 var validLocation = allValidLocations[i];
2896 if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) {
continue; }
2897 if (validLocation.EdgeCenter.Y <
AbyssArea.Bottom) {
continue; }
2898 var distanceSquaredToEdge = Vector2.DistanceSquared(pathPoint.Position, validLocation.EdgeCenter);
2900 if (distanceSquaredToEdge > 3.0f * (intervalRange.Y * intervalRange.Y)) {
continue; }
2902 if (distanceSquaredToEdge > Vector2.DistanceSquared(pathPoint.Position, validLocation.Cell.Center)) {
continue; }
2904 var validComparedToOtherPathPoints =
true;
2908 if (anotherPathPoint.Id == pathPoint.Id) {
continue; }
2909 if (Vector2.DistanceSquared(anotherPathPoint.Position, validLocation.EdgeCenter) < distanceSquaredToEdge)
2911 validComparedToOtherPathPoints =
false;
2916 foreach (var anotherPathPoint
in PathPoints.Where(p => p.Id != pathPoint.Id && p.ClusterLocations.Any()))
2918 if (!validComparedToOtherPathPoints) {
break; }
2919 foreach (var c
in pathPoint.ClusterLocations)
2921 if (IsInvalidComparedToExistingLocation())
2923 validComparedToOtherPathPoints =
false;
2927 bool IsInvalidComparedToExistingLocation()
2929 if (c.Equals(validLocation)) {
return true; }
2931 if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) {
return true; }
2934 if (MathUtils.LineSegmentsIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) {
return true; }
2940 if (!validComparedToOtherPathPoints) {
continue; }
2941 generatedCluster = CreateResourceCluster(pathPoint, validLocation);
2942 selectedLocationIndex = i;
2946 if (selectedLocationIndex >= 0)
2948 allValidLocations.RemoveAt(selectedLocationIndex);
2951 return generatedCluster;
2954 (e.NextToMainPath && t ==
TunnelType.MainPath) ||
2955 (e.NextToSidePath && t ==
TunnelType.SidePath) ||
2959 bool GenerateAdditionalCluster(PathPoint pathPoint)
2961 var validLocations =
new List<ClusterLocation>();
2964 foreach (var clusterLocation
in pathPoint.ClusterLocations)
2966 foreach (var anotherEdge
in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
2968 if (HaveConnectingEdgePoints(anotherEdge, clusterLocation.Edge))
2970 AddIfValid(clusterLocation.Cell, anotherEdge);
2977 if (validLocations.None())
2979 foreach (var clusterLocation
in pathPoint.ClusterLocations)
2981 foreach (var anotherEdge
in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
2983 var adjacentCell = anotherEdge.AdjacentCell(clusterLocation.Cell);
2984 if (adjacentCell ==
null) {
continue; }
2985 foreach (var adjacentCellEdge
in adjacentCell.Edges.Where(e => e != anotherEdge))
2987 if (HaveConnectingEdgePoints(adjacentCellEdge, clusterLocation.Edge))
2989 AddIfValid(adjacentCell, adjacentCellEdge);
2996 if (validLocations.Any())
2998 var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient);
2999 if (CreateResourceCluster(pathPoint, location))
3001 var i = allValidLocations.FindIndex(l => l.Equals(location));
3004 allValidLocations.RemoveAt(i);
3010 excludedPathPointIds.Add(pathPoint.Id);
3016 excludedPathPointIds.Add(pathPoint.Id);
3021 e1.Point1.NearlyEquals(e2.Point1) || e1.Point1.NearlyEquals(e2.Point2) ||
3022 e1.Point2.NearlyEquals(e2.Point1) || e1.Point2.NearlyEquals(e2.Point2);
3026 if (IsAlreadyInList(e)) {
return; }
3027 if (allValidLocations.None(l => l.Equals(c, e))) {
return; }
3028 if (pathPoint.ClusterLocations.Any(cl => cl.Edge == e)) {
return; }
3029 validLocations.Add(
new ClusterLocation(c, e));
3033 validLocations.Any(l => l.Edge == edge);
3036 bool CreateResourceCluster(PathPoint pathPoint, ClusterLocation location)
3038 if (location.Cell ==
null || location.Edge ==
null) {
return false; }
3040 ItemPrefab selectedPrefab;
3041 if (pathPoint.ClusterLocations.Count == 0)
3043 selectedPrefab = ToolBox.SelectWeightedRandom(
3044 levelResources.Select(it => it.itemPrefab).ToList(),
3045 levelResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
3046 Rand.RandSync.ServerAndClient);
3047 selectedPrefab.Tags.ForEach(t =>
3049 if (exclusiveResourceTags.Contains(t))
3051 pathPoint.ResourceTags.Add(t);
3057 var filteredResources = pathPoint.ResourceTags.None() ? levelResources :
3058 levelResources.Where(it => it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t)));
3059 selectedPrefab = ToolBox.SelectWeightedRandom(
3060 filteredResources.Select(it => it.itemPrefab).ToList(),
3061 filteredResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
3062 Rand.RandSync.ServerAndClient);
3065 if (selectedPrefab ==
null) {
return false; }
3068 float commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonnessInfo.GetCommonness(pathPoint.TunnelType);
3069 float lerpAmount = pathPoint.TunnelType !=
TunnelType.Cave ?
3070 MathUtils.InverseLerp(commonnessRange.X, commonnessRange.Y, commonness) :
3071 MathUtils.InverseLerp(caveCommonnessRange.X, caveCommonnessRange.Y, commonness);
3073 var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength);
3074 maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge);
3080 if (maxClusterSize < 1) {
return false; }
3083 var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient);
3085 if (resourcesInCluster < 1) {
return false; }
3087 PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLength: edgeLength);
3088 itemCount += resourcesInCluster;
3089 location.InitializeResources();
3090 location.Resources.AddRange(placedResources);
3091 pathPoint.ClusterLocations.Add(location);
3092 pathPoint.ResourceIds.Add(selectedPrefab.Identifier);
3097 int GetMaxResourcesOnEdge(ItemPrefab resourcePrefab, ClusterLocation location, out
float edgeLength)
3100 if (location.Cell ==
null || location.Edge ==
null) {
return 0; }
3101 edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
3102 if (resourcePrefab ==
null) {
return 0; }
3103 return (
int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X));
3110 var allValidLocations = GetAllValidClusterLocations();
3111 var placedResources =
new List<Item>();
3114 if (allValidLocations.None()) {
return placedResources; }
3117 for (
int i = allValidLocations.Count - 1; i >= 0; i--)
3119 if (HasResources(allValidLocations[i]))
3121 allValidLocations.RemoveAt(i);
3128 foreach (var c
in p.ClusterLocations)
3130 if (!c.Equals(clusterLocation)) {
continue; }
3131 foreach (var r
in c.Resources)
3133 if (r ==
null) {
continue; }
3134 if (r.Removed) {
continue; }
3135 if (!(r.GetComponent<
Holdable>() is
Holdable h) || (h.Attachable && h.Attached)) {
return true; }
3145 DebugConsole.AddWarning($
"Failed to find a position of the type \"{positionType}\" for mission resources.");
3148 if (validType != positionType &&
PositionsOfInterest.Any(p => p.PositionType == validType))
3150 DebugConsole.AddWarning($
"Placing in \"{validType}\" instead.");
3151 positionType = validType;
3159 RemoveInvalidLocations(positionType
switch
3165 _ =>
throw new NotImplementedException(),
3168 catch (NotImplementedException)
3170 DebugConsole.ThrowError($
"Unexpected PositionType (\"{positionType}\") for mineral mission resources: mineral spawning might not work as expected.");
3173 if (targetCaves !=
null && targetCaves.Any())
3176 allValidLocations.RemoveAll(l => targetCaves.None(c => c.Area.Contains(l.EdgeCenter)));
3179 var poi =
PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient);
3180 Vector2 poiPos = poi.Position.ToVector2();
3181 allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter)
3182 .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter)));
3183 float maxResourceOverlap = 0.4f;
3184 var selectedLocation = allValidLocations.FirstOrDefault(l =>
3185 Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is
float edgeLength &&
3186 !l.Edge.OutsideLevel &&
3187 ((l.Edge.Cell1?.IsDestructible ??
false) || (l.Edge.Cell2?.IsDestructible ??
false)) &&
3188 requiredAmount <= (
int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.
Size.X)));
3191 if (selectedLocation.Edge ==
null)
3194 float longestEdge = 0.0f;
3195 foreach (var validLocation
in allValidLocations)
3197 if (Vector2.Distance(validLocation.Edge.Point1, validLocation.Edge.Point2) is
float edgeLength && edgeLength > longestEdge)
3199 selectedLocation = validLocation;
3200 longestEdge = edgeLength;
3204 if (selectedLocation.Edge ==
null)
3206 throw new Exception(
"Failed to find a suitable level wall edge to place level resources on.");
3208 PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
3209 Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
3210 return placedResources;
3216 void RemoveInvalidLocations(Predicate<ClusterLocation> match)
3218 allValidLocations.RemoveAll(l => !match(l));
3222 private List<ClusterLocation> GetAllValidClusterLocations()
3224 var subBorders =
new List<Rectangle>();
3225 Wrecks.ForEach(AddBordersToList);
3228 var locations =
new List<ClusterLocation>();
3231 if (c.CellType !=
CellType.Solid) {
continue; }
3232 foreach (var e
in c.Edges)
3236 locations.Add(
new ClusterLocation(c, e));
3244 if (s ==
null) {
return; }
3247 rect.Inflate(4000, 4000);
3248 subBorders.Add(rect);
3253 if (!e.
IsSolid) {
return false; }
3256 if (IsBlockedByWreckOrBeacon()) {
return false; }
3257 if (IsBlockedByWall()) {
return false; }
3260 bool IsBlockedByWreckOrBeacon()
3262 foreach (var r
in subBorders)
3264 if (r.Contains(e.
Point1)) {
return true; }
3265 if (r.Contains(e.Point2)) {
return true; }
3266 if (r.Contains(eCenter)) {
return true; }
3268 if (MathUtils.GetLineRectangleIntersection(e.
Point1, e.Point2, r, out _))
3276 bool IsBlockedByWall()
3280 foreach (var c
in w.Cells)
3282 if (c.IsPointInside(eCenter)) {
return true; }
3283 if (c.IsPointInside(eCenter - 100 * e.
GetNormal(c))) {
return true; }
3284 if (c.Edges.Any(extraWallEdge => extraWallEdge == e)) {
return true; }
3292 private void PlaceResources(ItemPrefab resourcePrefab,
int resourceCount, ClusterLocation location, out List<Item> placedResources,
3293 float? edgeLength =
null,
float maxResourceOverlap = 0.4f)
3295 edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
3296 Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
3297 if (!MathUtils.IsValid(edgeDir))
3299 edgeDir = Vector2.Zero;
3301 var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
3302 minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
3303 var lerpAmounts =
new float[resourceCount];
3304 lerpAmounts[0] = 0.0f;
3305 var lerpAmount = 0.0f;
3306 for (
int i = 1; i < resourceCount; i++)
3308 var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
3309 lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
3310 lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
3312 var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
3313 placedResources =
new List<Item>();
3314 for (
int i = 0; i < resourceCount; i++)
3316 Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
3317 var item =
new Item(resourcePrefab, selectedPos, submarine:
null);
3318 Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
3319 float moveAmount = (item.body ==
null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
3320 moveAmount += (item.GetComponent<
LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
3321 item.Move(edgeNormal * moveAmount);
3325 item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
3327 else if (item.body !=
null)
3329 item.body.SetTransformIgnoreContacts(item.body.SimPosition, MathUtils.VectorToAngle(edgeNormal) - MathHelper.PiOver2);
3331 placedResources.Add(item);
3335 public Vector2
GetRandomItemPos(
PositionType spawnPosType,
float randomSpread,
float minDistFromSubs,
float offsetFromWall = 10.0f, Func<InterestingPosition, bool> filter =
null)
3339 return new Vector2(
Size.X / 2,
Size.Y / 2);
3342 Vector2 position = Vector2.Zero;
3348 Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
3349 Vector2 startPos = potentialPos.Position.ToVector2();
3355 Vector2 endPos = startPos - Vector2.UnitY *
Size.Y;
3358 if (!potentialPos.PositionType.IsEnclosedArea())
3361 ConvertUnits.ToSimUnits(startPos),
3362 ConvertUnits.ToSimUnits(endPos),
3364 Physics.CollisionLevel | Physics.CollisionWall)?.UserData is
VoronoiCell)
3375 position = startPos;
3378 }
while (tries < 10);
3386 bool success =
TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, awayPoint, minDistFromPoint, filter);
3393 bool success =
TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning);
3405 List<InterestingPosition> suitablePositions =
PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
3408 suitablePositions.RemoveAll(p => !filter(p));
3417 if (!suitablePositions.Contains(pos)) {
continue; }
3420 pos.IsValid =
false;
3425 suitablePositions.RemoveAll(p => IsInvalid(p));
3428 if (!suitablePositions.Any())
3430 if (!suppressWarning)
3432 string errorMsg =
"Could not find a suitable position of interest. (PositionType: " + positionType +
", minDistFromSubs: " + minDistFromSubs +
")\n" + Environment.StackTrace.CleanupStackTrace();
3433 GameAnalyticsManager.AddErrorEventOnce(
"Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3435 DebugConsole.ThrowError(errorMsg);
3442 List<InterestingPosition> farEnoughPositions =
new List<InterestingPosition>(suitablePositions);
3443 if (minDistFromSubs > 0.0f)
3448 farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.
WorldPosition) < minDistFromSubs * minDistFromSubs);
3451 if (minDistFromPoint > 0.0f)
3453 farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), awayPoint) < minDistFromPoint * minDistFromPoint);
3456 if (!farEnoughPositions.Any())
3458 string errorMsg =
"Could not find a position of interest far enough from the submarines. (PositionType: " + positionType +
", minDistFromSubs: " + minDistFromSubs +
")\n" + Environment.StackTrace.CleanupStackTrace();
3459 GameAnalyticsManager.AddErrorEventOnce(
"Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
3461 DebugConsole.ThrowError(errorMsg);
3463 float maxDist = 0.0f;
3464 position = suitablePositions.First();
3479 position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)];
3486 return closestCell !=
null && closestCell.IsPointInside(worldPosition);
3507 destructibleWall.NetworkUpdatePending =
false;
3510 networkUpdateTimer += deltaTime;
3511 if (networkUpdateTimer > NetworkUpdateInterval)
3513 if (
ExtraWalls.Any(w => w.Body.BodyType != BodyType.Static))
3517 networkUpdateTimer = 0.0f;
3523 backgroundCreatureManager.
Update(deltaTime, cam);
3525 renderer.
Update(deltaTime, cam);
3531 float interval =
Size.X / (bottomPositions.Count - 1);
3533 int index = (int)Math.Floor(xPosition / interval);
3534 if (index < 0 || index >= bottomPositions.Count - 1) {
return new Vector2(xPosition,
BottomPos); }
3536 float t = (xPosition - bottomPositions[index].X) / interval;
3537 t = MathHelper.Clamp(t, 0.0f, 1.0f);
3538 float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
3540 return new Vector2(xPosition, yPos);
3545 List<VoronoiCell> cells =
new List<VoronoiCell>();
3546 for (
int x = 0; x < cellGrid.GetLength(0); x++)
3548 for (
int y = 0; y < cellGrid.GetLength(1); y++)
3550 cells.AddRange(cellGrid[x, y]);
3556 private readonly List<VoronoiCell> tempCells =
new List<VoronoiCell>();
3557 public List<VoronoiCell>
GetCells(Vector2 worldPos,
int searchDepth = 2)
3560 int gridPosX = (int)Math.Floor(worldPos.X /
GridCellSize);
3561 int gridPosY = (int)Math.Floor(worldPos.Y /
GridCellSize);
3563 int startX = Math.Max(gridPosX - searchDepth, 0);
3564 int endX = Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1);
3566 int startY = Math.Max(gridPosY - searchDepth, 0);
3567 int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1);
3569 for (
int y = startY; y <= endY; y++)
3571 for (
int x = startX; x <= endX; x++)
3573 tempCells.AddRange(cellGrid[x, y]);
3586 bool closeEnough =
false;
3595 if (!closeEnough) {
continue; }
3599 tempCells.Add(cell);
3605 if (abyssIsland.Area.X > worldPos.X + searchDepth *
GridCellSize) {
continue; }
3606 if (abyssIsland.Area.Right < worldPos.X - searchDepth *
GridCellSize) {
continue; }
3607 if (abyssIsland.Area.Y > worldPos.Y + searchDepth *
GridCellSize) {
continue; }
3608 if (abyssIsland.Area.Bottom < worldPos.Y - searchDepth *
GridCellSize) {
continue; }
3610 tempCells.AddRange(abyssIsland.Cells);
3618 double closestDist =
double.MaxValue;
3620 int searchDepth = 2;
3621 while (searchDepth < 5)
3623 foreach (var cell
in GetCells(worldPos, searchDepth))
3625 double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, worldPos.X, worldPos.Y);
3626 if (dist < closestDist)
3632 if (closestCell !=
null) {
break; }
3638 private List<VoronoiCell> CreatePathToClosestTunnel(Point pos)
3641 double closestDist = 0.0f;
3642 foreach (Tunnel tunnel
in Tunnels)
3644 if (tunnel.Type ==
TunnelType.Cave) {
continue; }
3647 double dist = MathUtils.DistanceSquared(cell.
Site.
Coord.
X, cell.
Site.
Coord.Y, pos.X, pos.Y);
3648 if (closestPathCell ==
null || dist < closestDist)
3650 closestPathCell = cell;
3657 List<VoronoiCell> validCells = cells.FindAll(c => c.CellType !=
CellType.Empty && c.CellType !=
CellType.Removed);
3658 List<VoronoiCell> pathCells =
new List<VoronoiCell>() { closestPathCell };
3663 if (!MathUtils.LineSegmentsIntersect(closestPathCell.
Center, pos.ToVector2(), e.
Point1, e.Point2)) {
continue; }
3666 for (
int x = 0; x < cellGrid.GetLength(0); x++)
3668 for (
int y = 0; y < cellGrid.GetLength(1); y++)
3670 cellGrid[x, y].Remove(cell);
3673 pathCells.Add(cell);
3677 foreach (var otherEdge
in cell.
Edges)
3679 var otherAdjacent = otherEdge.AdjacentCell(cell);
3680 if (otherAdjacent ==
null || otherAdjacent.CellType ==
CellType.Solid) {
continue; }
3683 if (Vector2.DistanceSquared(otherEdge.Point1, otherEdge.Point2) < 500.0f * 500.0f)
3687 if (e2 == otherEdge || e2 == otherEdge) {
continue; }
3688 if (!MathUtils.NearlyEqual(otherEdge.Point1, e2.
Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.
Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point2))
3693 if (adjacentCell ==
null || adjacentCell.CellType ==
CellType.Removed) {
continue; }
3695 for (
int x = 0; x < cellGrid.GetLength(0); x++)
3697 for (
int y = 0; y < cellGrid.GetLength(1); y++)
3699 cellGrid[x, y].Remove(adjacentCell);
3702 cells.Remove(adjacentCell);
3710 pathCells.Sort((c1, c2) => {
return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); });
3719 return MathUtils.LineSegmentToPointDistanceSquared(startPosition, startExitPosition, position) < minDist * minDist;
3724 return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist;
3729 var tempSW =
new Stopwatch();
3734 wp.Submarine ==
null &&
3738 !
IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
3752 Rectangle paddedBorders =
new Rectangle(
3753 subBorders.X - padding,
3754 subBorders.Y + padding,
3755 subBorders.Width + padding * 2,
3756 subBorders.Height + padding * 2);
3758 var positions =
new List<Vector2>();
3759 var rects =
new List<Rectangle>();
3760 int maxAttempts = 50;
3761 int attemptsLeft = maxAttempts;
3762 bool success =
false;
3763 Vector2 spawnPoint = Vector2.Zero;
3765 while (attemptsLeft > 0)
3767 if (attemptsLeft < maxAttempts)
3769 Debug.WriteLine($
"Failed to position the sub {subName}. Trying again.");
3772 if (TryGetSpawnPoint(out spawnPoint))
3774 success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
3786 DebugConsole.NewMessage($
"Failed to find any spawn point for the sub: {subName} (No valid waypoints left).", Color.Red);
3793 Debug.WriteLine($
"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
3800 PositionsOfInterest.Add(
new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
3801 foreach (Hull hull
in sub.GetHulls(
false))
3812 if (!sub.CreateWreckAI())
3814 DebugConsole.NewMessage($
"Failed to create wreck AI inside {subName}.", Color.Red);
3815 sub.DisableWreckAI();
3820 sub.DisableWreckAI();
3825 PositionsOfInterest.Add(
new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
3827 sub.ShowSonarMarker =
false;
3828 sub.DockedTo.ForEach(s => s.ShowSonarMarker =
false);
3829 sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
3833 Debug.WriteLine($
"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
3834 sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines:
false);
3835 wreckPositions.Add(sub, positions);
3836 blockedRects.Add(sub, rects);
3841 DebugConsole.NewMessage($
"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).", Color.Red);
3845 bool TryPositionSub(Rectangle subBorders,
string subName,
PlacementType placement, ref Vector2 spawnPoint)
3847 positions.Add(spawnPoint);
3848 bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
3849 positions.Add(spawnPoint);
3851 bool leftSideBlocked = IsSideBlocked(subBorders,
false);
3852 bool rightSideBlocked = IsSideBlocked(subBorders,
true);
3854 if (rightSideBlocked && !leftSideBlocked)
3856 bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
3858 else if (leftSideBlocked && !rightSideBlocked)
3860 bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
3862 else if (!bottomFound)
3864 if (!leftSideBlocked)
3866 bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
3868 else if (!rightSideBlocked)
3870 bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
3874 Debug.WriteLine($
"Invalid position {spawnPoint}. Does not touch the ground.");
3878 positions.Add(spawnPoint);
3881 int shrinkAmount = step + 50;
3883 subBorders.X + shrinkAmount,
3884 subBorders.Y - shrinkAmount,
3885 subBorders.Width - shrinkAmount * 2,
3886 subBorders.Height - shrinkAmount * 2);
3887 bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders);
3890 rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size));
3891 Debug.WriteLine($
"Invalid position {spawnPoint}. Blocked by level walls.");
3893 else if (!bottomFound)
3895 Debug.WriteLine($
"Invalid position {spawnPoint}. Does not touch the ground.");
3899 var sp = spawnPoint;
3900 if (
Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
3902 Debug.WriteLine($
"Invalid position {spawnPoint}. Too close to other wreck(s).");
3906 return !isBlocked && bottomFound;
3908 bool TryMove(Rectangle subBorders,
PlacementType placement, ref Vector2 spawnPoint,
float amount)
3910 float maxMovement = 5000;
3911 float totalAmount = 0;
3912 bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
3914 while (IsSideBlocked(subBorders, front: amount < 0))
3916 foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
3917 totalAmount += amount;
3918 spawnPoint =
new Vector2(spawnPoint.X + amount, spawnPoint.Y);
3919 if (Math.Abs(totalAmount) > maxMovement)
3921 Debug.WriteLine($
"Moving the sub {subName} failed.");
3929 bool TryGetSpawnPoint(out Vector2 spawnPoint)
3931 spawnPoint = Vector2.Zero;
3932 while (waypoints.Any())
3934 var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
3935 waypoints.Remove(wp);
3936 if (!IsBlocked(wp.WorldPosition, paddedBorders))
3938 spawnPoint = wp.WorldPosition;
3945 bool TryRaycast(Rectangle subBorders,
PlacementType placement, ref Vector2 spawnPoint)
3949 var hitPositions =
new Vector2[rayCount];
3951 for (
int i = 0; i < rayCount; i++)
3956 subBorders.Width * MathHelper.Lerp(0.2f, 0.8f, i / (
float)(rayCount - 1));
3957 Vector2 rayStart =
new Vector2(
3958 spawnPoint.X + subBorders.Location.X + xOffset,
3960 var simPos = ConvertUnits.ToSimUnits(rayStart);
3963 collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
3971 float highestPoint = placement ==
PlacementType.Bottom ? hitPositions.Max(p => p.Y) : hitPositions.Min(p => p.Y);
3972 float halfHeight = subBorders.Height / 2;
3973 float centerOffset = subBorders.Y - halfHeight;
3974 spawnPoint =
new Vector2(spawnPoint.X, highestPoint + halfHeight * -dir - centerOffset);
3978 bool IsSideBlocked(Rectangle subBorders,
bool front)
3980 Point centerOffset =
new Point(subBorders.Center.X, subBorders.Y - subBorders.Height / 2);
3984 int dir = front ? 1 : -1;
3985 Vector2 halfSize = subBorders.Size.ToVector2() / 2;
3986 Vector2 quarterSize = halfSize / 2;
3987 for (
int i = 0; i < rayCount; i++)
3989 Vector2 rayStart, to;
3994 float yOffset = quarterSize.Y * (i == 1 ? 1 : -1);
3997 rayStart =
new Vector2(spawnPoint.X + quarterSize.X * dir, spawnPoint.Y + yOffset);
3998 to =
new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
4003 rayStart = spawnPoint;
4004 to =
new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
4008 rayStart += centerOffset.ToVector2();
4009 to += centerOffset.ToVector2();
4011 Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
4013 customPredicate: f => f.Body?.UserData is
VoronoiCell cell,
4014 collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
4015 allowInsideFixture:
true) !=
null)
4023 bool IsBlocked(Vector2 pos, Rectangle submarineBounds)
4026 (
int)pos.X + submarineBounds.X, (
int)pos.Y + submarineBounds.Y,
4027 submarineBounds.Width, submarineBounds.Height);
4028 if (
Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds)))
4033 ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) ||
4034 ToolBox.GetWorldBounds(c.StartPos,
new Point(1500)).IntersectsWorld(bounds)))
4038 return cells.Any(c =>
4040 c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
4045 private readonly Dictionary<Submarine, List<Vector2>> wreckPositions =
new Dictionary<Submarine, List<Vector2>>();
4046 private readonly Dictionary<Submarine, List<Rectangle>> blockedRects =
new Dictionary<Submarine, List<Rectangle>>();
4048 private readonly record
struct PlaceableWreck(WreckFile WreckFile, WreckInfo WreckInfo)
4050 public static Option<PlaceableWreck> TryCreate(WreckFile wreckFile)
4052 var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == wreckFile.Path.Value);
4053 if (matchingSub?.WreckInfo is
null)
4055 DebugConsole.ThrowError($
"No matching submarine info found for the wreck file {wreckFile.Path.Value}");
4059 return Option.Some(
new PlaceableWreck(wreckFile, matchingSub.WreckInfo));
4063 private void CreateWrecks()
4065 var totalSW =
new Stopwatch();
4068 var placeableWrecks = ContentPackageManager.EnabledPackages.All
4069 .SelectMany(p => p.GetFiles<WreckFile>())
4070 .OrderBy(f => f.UintIdentifier)
4071 .Select(PlaceableWreck.TryCreate)
4072 .Where(w => w.IsSome())
4073 .Select(o => o.TryUnwrap(out var w) ? w :
throw new InvalidOperationException())
4076 for (
int i = placeableWrecks.Count - 1; i >= 0; i--)
4078 var wreckInfo = placeableWrecks[i].WreckInfo;
4081 placeableWrecks.RemoveAt(i);
4084 if (placeableWrecks.None())
4086 DebugConsole.ThrowError($
"No wreck files found for the level difficulty {LevelData.Difficulty}!");
4087 Wrecks =
new List<Submarine>();
4090 placeableWrecks.Shuffle(Rand.RandSync.ServerAndClient);
4094 int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient);
4095 bool requireThalamus =
false;
4097 if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ??
false)
4099 wreckCount = Math.Max(wreckCount, 1);
4102 if (GameMain.GameSession?.GameMode?.Missions.Any(
static m => m.Prefab.RequireThalamusWreck) ??
false)
4104 requireThalamus =
true;
4111 if (matchingFile.WreckFile !=
null)
4113 placeableWrecks.Remove(matchingFile);
4114 placeableWrecks.Insert(0, matchingFile);
4116 wreckCount = Math.Max(wreckCount, 1);
4119 if (requireThalamus)
4121 var thalamusWrecks = placeableWrecks
4122 .Where(
static w => w.WreckInfo.WreckContainsThalamus == WreckInfo.HasThalamus.Yes)
4125 if (thalamusWrecks.Any())
4127 thalamusWrecks.Shuffle(Rand.RandSync.ServerAndClient);
4129 foreach (var wreck
in thalamusWrecks)
4131 placeableWrecks.Remove(wreck);
4132 placeableWrecks.Insert(0, wreck);
4137 Wrecks =
new List<Submarine>(wreckCount);
4138 for (
int i = 0; i < wreckCount; i++)
4141 const int MaxSubsToTry = 2;
4143 while (placeableWrecks.Any() && attempts < MaxSubsToTry)
4145 var placeableWreck = placeableWrecks.First();
4146 var wreckFile = placeableWreck.WreckFile;
4147 placeableWrecks.RemoveAt(0);
4148 if (wreckFile ==
null) {
continue; }
4149 string wreckName = System.IO.Path.GetFileNameWithoutExtension(wreckFile.Path.Value);
4150 if (SpawnSubOnPath(wreckName, wreckFile,
SubmarineType.Wreck, forceThalamus: requireThalamus) is { } wreck)
4152 if (wreck.WreckAI is not
null)
4154 requireThalamus =
false;
4164 Debug.WriteLine($
"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)");
4167 private bool HasStartOutpost()
4169 if (preSelectedStartOutpost !=
null) {
return true; }
4174 if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible())
4179 if (!IsModeStartOutpostCompatible())
4192 private bool HasEndOutpost()
4194 if (preSelectedEndOutpost !=
null) {
return true; }
4201 private void CreateOutposts()
4203 var outpostFiles = ContentPackageManager.EnabledPackages.All
4204 .SelectMany(p => p.GetFiles<OutpostFile>())
4205 .OrderBy(f => f.UintIdentifier).ToList();
4208 DebugConsole.ThrowError(
"No outpost files found in the selected content packages");
4212 for (
int i = 0; i < 2; i++)
4214 if (GameMain.GameSession?.GameMode is PvPMode) {
continue; }
4216 bool isStart = (i == 0) == !
Mirrored;
4219 if (!HasStartOutpost()) {
continue; }
4223 if (!HasEndOutpost()) {
continue; }
4226 SubmarineInfo outpostInfo;
4229 SubmarineInfo preSelectedOutpost = isStart ? preSelectedStartOutpost : preSelectedEndOutpost;
4230 if (preSelectedOutpost ==
null)
4235 OutpostGenerationParams outpostGenerationParams =
null;
4242 outpostGenerationParams =
4247 LocationType locationType = location?.Type;
4248 if (locationType ==
null)
4250 locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
4251 if (outpostGenerationParams.AllowedLocationTypes.Any())
4253 locationType = LocationType.Prefabs.GetRandom(lt =>
4254 outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
4255 allowedType ==
"any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient);
4259 if (location !=
null)
4261 DebugConsole.NewMessage($
"Generating an outpost for the {(isStart ? "start
" : "end
")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
4266 DebugConsole.NewMessage($
"Generating an outpost for the {(isStart ? "start
" : "end
")} of the level... (Location type: {locationType}, level type: {LevelData.Type})");
4270 foreach (
string categoryToHide
in locationType.HideEntitySubcategories)
4272 foreach (MapEntity entityToHide
in MapEntity.MapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ??
false)))
4274 entityToHide.IsLayerHidden =
true;
4280 DebugConsole.NewMessage($
"Loading a pre-built outpost for the {(isStart ? "start
" : "end
")} of the level...");
4282 ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient);
4283 outpostInfo =
new SubmarineInfo(outpostFile.Path.Value)
4292 DebugConsole.NewMessage($
"Loading a pre-selected outpost for the {(isStart ? "start
" : "end
")} of the level...");
4293 outpostInfo = preSelectedOutpost;
4298 Point? minSize =
null;
4300 float closestDistance =
float.MaxValue;
4304 Point outpostSize = outpost.GetDockedBorders().Size;
4305 minSize =
new Point(Math.Max(subSize.X, outpostSize.X), subSize.Y + outpostSize.Y);
4317 closestDistance = dist;
4330 closestDistance =
float.MaxValue;
4338 if (dist < closestDistance)
4341 closestDistance = dist;
4347 if (Math.Abs(subDockingPortOffset) > 5000.0f)
4349 subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f);
4350 string warningMsg =
"Docking port very far from the sub's center of mass (submarine: " +
Submarine.
MainSub.
Info.
Name +
", dist: " + subDockingPortOffset +
"). The level generator may not be able to place the outpost so that docking is possible.";
4351 DebugConsole.NewMessage(warningMsg, Color.Orange);
4352 GameAnalyticsManager.AddErrorEventOnce(
"Lever.CreateOutposts:DockingPortVeryFar" +
Submarine.
MainSub.
Info.
Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
4355 float? outpostDockingPortOffset =
null;
4356 if (outpostPort !=
null)
4358 outpostDockingPortOffset = subPort ==
null ? 0.0f : outpostPort.
Item.
WorldPosition.X - outpost.WorldPosition.X;
4360 if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f)
4362 outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f);
4363 string warningMsg =
"Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name +
", dist: " + outpostDockingPortOffset +
"). The level generator may not be able to place the outpost so that docking is possible.";
4364 DebugConsole.NewMessage(warningMsg, Color.Orange);
4365 GameAnalyticsManager.AddErrorEventOnce(
"Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
4376 preferredSpawnPos.X >
Size.X * 0.75f &&
4377 preferredSpawnPos.Y <
Size.Y * 0.25f)
4379 preferredSpawnPos.X = (preferredSpawnPos.X +
Size.X) / 2;
4382 spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset !=
null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
4385 spawnPos.Y = Math.Min(
Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
4389 outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines:
false);
4390 SetLinkedSubCrushDepth(outpost);
4392 foreach (WayPoint wp
in WayPoint.WayPointList)
4394 if (wp.Submarine == outpost && wp.SpawnType !=
SpawnType.Path)
4396 PositionsOfInterest.Add(
new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost));
4421 private void CreateBeaconStation()
4424 var beaconStationFiles = ContentPackageManager.EnabledPackages.All
4425 .SelectMany(p => p.GetFiles<BeaconStationFile>())
4426 .OrderBy(f => f.UintIdentifier).ToList();
4427 if (beaconStationFiles.None())
4429 DebugConsole.ThrowError(
"No BeaconStation files found in the selected content packages!");
4433 var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
4434 ContentFile contentFile =
null;
4438 contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == contentPath);
4439 if (contentFile ==
null)
4441 DebugConsole.ThrowError($
"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead...");
4449 if (contentFile ==
null)
4451 for (
int i = beaconStationFiles.Count - 1; i >= 0; i--)
4453 var beaconStationFile = beaconStationFiles[i];
4454 var matchingInfo = beaconInfos.SingleOrDefault(info => info.
FilePath == beaconStationFile.Path.Value);
4455 Debug.Assert(matchingInfo !=
null);
4456 if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
4460 beaconStationFiles.RemoveAt(i);
4464 if (beaconStationFiles.None())
4466 DebugConsole.ThrowError($
"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
4469 contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
4472 string beaconStationName = System.IO.
Path.GetFileNameWithoutExtension(contentFile.Path.Value);
4481 if (sonarItem ==
null)
4483 DebugConsole.ThrowError($
"No sonar found in the beacon station \"{beaconStationName}\"!");
4486 beaconSonar = sonarItem.GetComponent<
Sonar>();
4496 throw new InvalidOperationException(
"Failed to prepare beacon station (no beacon station in the level).");
4501 Item reactorItem = beaconItems.Find(it => it.GetComponent<
Reactor>() !=
null);
4502 Reactor reactorComponent =
null;
4504 if (reactorItem !=
null)
4506 reactorComponent = reactorItem.GetComponent<
Reactor>();
4508 reactorContainer = reactorItem.GetComponent<
ItemContainer>();
4510 if (repairable !=
null)
4525 if (beaconSonar ==
null)
4527 DebugConsole.AddWarning($
"Beacon station \"{BeaconStation.Info.Name}\" has no sonar. Beacon missions might not work correctly with this beacon station.");
4533 beaconSonar.
Item.CreateServerEvent(beaconSonar);
4539 bool allowDisconnectedWires =
true;
4540 bool allowDamagedDevices =
true;
4541 bool allowDamagedWalls =
true;
4544 allowDisconnectedWires = info.AllowDisconnectedWires;
4545 allowDamagedWalls = info.AllowDamagedWalls;
4546 allowDamagedDevices = info.AllowDamagedDevices;
4550 float disconnectWireMinDifficulty = 20.0f;
4551 float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f,
LevelData.
Difficulty) * 0.5f;
4552 if (disconnectWireProbability > 0.0f && allowDisconnectedWires)
4557 if (allowDamagedDevices)
4561 if (allowDamagedWalls)
4571 if (disconnectWireProbability <= 0.0f) {
return; }
4573 foreach (
Item item
in beaconItems.Where(it => it.GetComponent<
Wire>() !=
null).ToList())
4576 Wire wire = item.GetComponent<
Wire>();
4577 if (wire.
Locked) {
continue; }
4586 if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < disconnectWireProbability)
4590 if (connection !=
null)
4596 wire.CreateNetworkEvent();
4606 if (breakDeviceProbability <= 0.0f) {
return; }
4609 foreach (
Item item
in beaconItems.Where(it => it.Components.Any(c => c is
Powered) && it.Components.Any(c => c is
Repairable)))
4612 if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < breakDeviceProbability)
4614 item.
Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
4621 if (damageWallProbability <= 0.0f) {
return; }
4625 if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < damageWallProbability)
4627 int sectionIndex = Rand.Range(0, structure.
SectionCount - 1, Rand.RandSync.Unsynced);
4635 if (beaconSonar ==
null) {
return false; }
4639 private void SetLinkedSubCrushDepth(
Submarine parentSub)
4643 connectedSub.SetCrushDepth(Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000));
4647 private static bool IsModeStartOutpostCompatible()
4650 return GameMain.GameSession?.GameMode is CampaignMode || GameMain.GameSession?.GameMode is TutorialMode || GameMain.GameSession?.GameMode is TestGameMode;
4652 return GameMain.GameSession?.GameMode is CampaignMode;
4662 int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
4663 var allSpawnPoints =
WayPoint.
WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull !=
null);
4664 var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType ==
SpawnType.Path);
4665 var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType ==
SpawnType.Corpse);
4666 if (!corpsePoints.Any() && !pathPoints.Any()) {
continue; }
4667 pathPoints.Shuffle(Rand.RandSync.Unsynced);
4669 corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob ==
null).ThenBy(p => Rand.Value()).ToList();
4670 var usedJobs =
new HashSet<JobPrefab>();
4671 int spawnCounter = 0;
4672 for (
int j = 0; j < corpseCount; j++)
4674 WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault();
4679 selectedPrefab = GetCorpsePrefab(usedJobs);
4683 selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job ==
"any" || p.Job == job.
Identifier);
4684 if (selectedPrefab ==
null)
4686 corpsePoints.Remove(sp);
4687 pathPoints.Remove(sp);
4688 sp = corpsePoints.FirstOrDefault(sp => sp.
AssignedJob ==
null) ?? pathPoints.FirstOrDefault(sp => sp.
AssignedJob ==
null);
4690 selectedPrefab = GetCorpsePrefab(usedJobs);
4691 if (selectedPrefab !=
null)
4697 if (selectedPrefab ==
null) {
continue; }
4701 if (!TryGetExtraSpawnPoint(out worldPos))
4709 corpsePoints.Remove(sp);
4710 pathPoints.Remove(sp);
4713 job ??= selectedPrefab.
GetJobPrefab(predicate: p => !usedJobs.Contains(p));
4714 if (job ==
null) {
continue; }
4724 corpse.EnableDespawn =
false;
4725 selectedPrefab.
GiveItems(corpse, wreck, sp);
4726 corpse.Kill(
CauseOfDeathType.Unknown, causeOfDeathAffliction:
null, log:
false);
4728 bool applyBurns = Rand.Value() < 0.1f;
4729 bool applyDamage = Rand.Value() < 0.3f;
4730 foreach (var limb
in corpse.AnimController.Limbs)
4732 if (applyDamage && (limb.type ==
LimbType.Head || Rand.Value() < 0.5f))
4735 float max = prefab.
MaxStrength / prefab.DamageOverlayAlpha;
4736 corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
4741 float max = prefab.
MaxStrength / prefab.BurnOverlayAlpha;
4742 corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
4745 static float GetStrength(
Limb limb,
float max)
4747 float strength = Rand.Range(0, max);
4750 strength = Math.Min(strength, Rand.Range(0, max));
4755 corpse.CharacterHealth.ForceUpdateVisuals();
4758 if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
4760 corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
4765 static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate =
null)
4768 usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) &&
4770 (predicate ==
null || predicate(p)));
4772 return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced);
4776 DebugConsole.NewMessage($
"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow);
4778 bool TryGetExtraSpawnPoint(out Vector2 point)
4780 point = Vector2.Zero;
4781 var hull =
Hull.
HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced();
4784 point = hull.WorldPosition;
4786 return hull !=
null;
4798 OutpostGenerator.SpawnNPCs(StartLocation, sub);
4811 return (-(worldPositionY - GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio;
4815 return (-(worldPositionY - GenerationParams.Height) +
LevelData.
InitialDepth) * Physics.DisplayToRealWorldRatio;
4821 StartLocation = newStartLocation;
4826 EndLocation = newEndLocation;
4833 if (renderer !=
null)
4846 AbyssIslands?.Clear();
4847 AbyssResources?.Clear();
4850 PathPoints?.Clear();
4851 PositionsOfInterest?.Clear();
4853 wreckPositions?.Clear();
4858 StartOutpost =
null;
4861 blockedRects?.Clear();
4863 EntitiesBeforeGenerate?.Clear();
4864 ClearEqualityCheckValues();
4872 bottomPositions?.Clear();
4873 BottomBarrier =
null;
4877 distanceField =
null;
4879 if (ExtraWalls !=
null)
4884 if (UnsyncedExtraWalls !=
null)
4887 UnsyncedExtraWalls =
null;
4900 StartLocation =
null;
4907 static class PositionTypeExtensions
4912 public static bool IsEnclosedArea(
this Level.PositionType positionType)
4915 positionType == Level.PositionType.Cave ||
4916 positionType == Level.PositionType.AbyssCave ||
4917 positionType.IsIndoorsArea();
4923 public static bool IsIndoorsArea(
this Level.PositionType positionType)
4926 positionType == Level.PositionType.Outpost ||
4927 positionType == Level.PositionType.BeaconStation ||
4928 positionType == Level.PositionType.Ruin ||
4929 positionType == Level.PositionType.Wreck;
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
Affliction Instantiate(float strength, Character source=null)
static AfflictionPrefab OxygenLow
static AfflictionPrefab Burn
readonly float MaxStrength
The maximum strength this affliction can have.
static AfflictionPrefab BiteWounds
void SpawnCreatures(Level level, int count, Vector2? position=null)
void Update(float deltaTime, Camera cam)
Level.PlacementType Placement
readonly float MinDifficulty
float AdjustedMaxDifficulty
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
readonly AnimController AnimController
Stores information about the Character that is needed between rounds in the menu etc....
static readonly Identifier HumanSpeciesName
Base class for content file types, which are loaded from filelist.xml via reflection....
readonly ContentPath Path
static readonly PrefabCollection< CorpsePrefab > Prefabs
static EntitySpawner Spawner
virtual Vector2 WorldPosition
static IReadOnlyCollection< Entity > GetEntities()
void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition=null, int? quality=null, Action< Item > onSpawned=null)
static GameSession?? GameSession
static bool IsSingleplayer
static NetworkMember NetworkMember
static readonly List< Hull > HullList
JobPrefab GetJobPrefab(Rand.RandSync randSync=Rand.RandSync.Unsynced, Func< JobPrefab, bool > predicate=null)
bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync=Rand.RandSync.Unsynced, bool createNetworkEvents=true)
bool InvulnerableToDamage
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
ConnectionPanel ConnectionPanel
readonly HashSet< Wire > DisconnectedWires
Wires that have been disconnected from the panel, but not removed completely (visible at the bottom o...
static IEnumerable< DockingPort > List
readonly ItemInventory Inventory
ImmutableHashSet< Identifier > ContainableItemIdentifiers
void PowerUpImmediately()
float FuelConsumptionRate
const float DefaultSonarRange
const float AutopilotMinDistToPathNode
void RemoveConnection(Item item)
readonly List< VoronoiCell > Cells
AbyssIsland(Rectangle area, List< VoronoiCell > cells)
readonly HashSet< Mission > MissionsToDisplayOnSonar
readonly CaveGenerationParams CaveGenerationParams
Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos)
readonly List< Tunnel > Tunnels
Tunnel(TunnelType type, List< Point > nodes, int minWidth, Tunnel parentTunnel)
List< WayPoint > WayPoints
readonly Tunnel ParentTunnel
List< VoronoiCell > Cells
int? MinMainPathWidth
Determined during level generation based on the size of the submarine. Null if the level hasn't been ...
SubmarineInfo ForceBeaconStation
float CrushDepth
The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the t...
OutpostGenerationParams ForceOutpostGenerationParams
LevelGenerationParams GenerationParams
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
readonly float Difficulty
bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty)
Inclusive (matching the min an max values is accepted).
readonly int InitialDepth
The depth at which the level starts at, in in-game coordinates. E.g. if this was set to 100 000 (= 10...
static IEnumerable< OutpostGenerationParams > GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
bool OutpostGenerationParamsExist
float ResourceSpawnChance
float ThalamusProbability
float CaveResourceSpawnChance
float WreckFloodingHullMinWaterPercentage
float WreckFloodingHullMaxWaterPercentage
int FloatingIceChunkCount
float WreckHullFloodingChance
int AbyssResourceClustersMax
Point ResourceClusterSizeRange
readonly ImmutableHashSet< Identifier > AllowedBiomeIdentifiers
Point MinSideTunnelRadius
Point CaveResourceIntervalRange
Point MainPathNodeIntervalRange
int BackgroundCreatureAmount
int AbyssResourceClustersMin
string ForceBeaconStation
float AbyssIslandCaveProbability
readonly bool AnyBiomeAllowed
float BottomHoleProbability
Color BackgroundTextureColor
Point ResourceIntervalRange
Point VoronoiSiteInterval
int CellSubdivisionLength
bool UseRandomRuinCount()
Point VoronoiSiteVariance
Vector2 ForceOutpostPosition
static ? float ForcedDifficulty
bool ShouldSpawnCrewInsideOutpost()
LevelGenerationParams GenerationParams
const float OutsideBoundsCurrentMargin
How far outside the boundaries of the level the water current that pushes subs towards the level star...
List< double > siteCoordsX
bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Func< InterestingPosition, bool > filter=null, bool suppressWarning=false)
int EntityCountAfterGenerate
void DamageBeaconStationWalls(float damageWallProbability)
const float OutsideBoundsCurrentHardLimit
How far outside the boundaries of the level the current stops submarines entirely
LevelGenStage
Random integers generated during the level generation. If these values differ between clients/server,...
LevelObjectManager LevelObjectManager
Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall=10.0f, Func< InterestingPosition, bool > filter=null)
readonly LevelData LevelData
List< ClusterLocation > AbyssResources
VoronoiCell GetClosestCell(Vector2 worldPos)
List< AbyssIsland > AbyssIslands
List< PathPoint > PathPoints
Color BackgroundTextureColor
bool IsCloseToEnd(Vector2 position, float minDist)
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
void DebugSetEndLocation(Location newEndLocation)
void DamageBeaconStationDevices(float breakDeviceProbability)
bool IsCloseToEnd(Point position, float minDist)
BackgroundCreatureManager BackgroundCreatureManager
bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint, Func< InterestingPosition, bool > filter=null)
const int MaxSubmarineWidth
The level generator won't try to adjust the width of the main path above this limit.
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
int EntityCountBeforeGenerate
bool IsCloseToStart(Vector2 position, float minDist)
bool IsCloseToStart(Point position, float minDist)
bool IsPositionInsideWall(Vector2 worldPosition)
List< VoronoiCell > GetAllCells()
List<(Point point, double distance)> distanceField
IReadOnlyDictionary< LevelGenStage, int > EqualityCheckValues
float CrushDepth
The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the t...
bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint=0f, Func< InterestingPosition, bool > filter=null, bool suppressWarning=false)
Vector2 GetBottomPosition(float xPosition)
void DebugSetStartLocation(Location newStartLocation)
void Update(float deltaTime, Camera cam)
static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost=null, SubmarineInfo endOutpost=null)
List< Item > GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable< Cave > targetCaves=null)
Used by clients to set the rotation for the resources
const float OutsideBoundsCurrentMarginExponential
How far outside the boundaries of the level the strength of the current starts to increase exponentia...
float RealWorldCrushDepth
The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa)...
List< LevelWall > UnsyncedExtraWalls
List< Entity > EntitiesBeforeGenerate
void PrepareBeaconStation()
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
List< VoronoiCell > GetTooCloseCells(Vector2 position, float minDistance)
const float DefaultRealWorldCrushDepth
void DisconnectBeaconStationWires(float disconnectWireProbability)
bool IsAllowedDifficulty(float minDifficulty, float maxDifficulty)
Inclusive (matching the min an max values is accepted).
Vector2 StartExitPosition
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
List< LevelWall > ExtraWalls
List< InterestingPosition > PositionsOfInterest
void PlaceObjects(Level level, int amount)
void Update(float deltaTime)
void SetVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color)
void Update(float deltaTime, Camera cam)
List< VoronoiCell > Cells
virtual void Update(float deltaTime)
LocalizedString DisplayName
readonly CharacterTeamType OutpostTeam
static readonly PrefabCollection< LocationType > Prefabs
static readonly ImmutableArray< PositionType > ValidPositionTypes
bool SpawnCrewInsideOutpost
ContentPackage? ContentPackage
readonly Identifier Identifier
void FindHull(Vector2? worldPosition=null, bool setSubmarine=true)
static readonly PrefabCollection< RuinGenerationParams > RuinParams
List< VoronoiCell > PathCells
void AddDamage(int sectionIndex, float damage, Character attacker=null, bool emitParticles=true, bool createWallDamageProjectiles=false)
static List< Structure > WallList
static readonly Submarine[] MainSubs
override Vector2? WorldPosition
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static List< Submarine > Loaded
static Rectangle GetBorders(XElement submarineElement)
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
static Rectangle AbsRect(Vector2 pos, Vector2 size)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
static Vector2 LastPickedPosition
OutpostGenerationParams OutpostGenerationParams
static XDocument OpenFile(string file)
BeaconStationInfo BeaconStationInfo
void ScrollWater(Vector2 vel, float deltaTime)
static WaterRenderer Instance
static List< WayPoint > WayPointList
VoronoiCell AdjacentCell(VoronoiCell cell)
Vector2 GetNormal(VoronoiCell cell)
Returns the normal of the edge that points outwards from the specified cell
bool IsPointInsideAABB(Vector2 point2, float margin)
bool IsPointInside(Vector2 point)
List< GraphEdge > MakeVoronoiGraph(List< Vector2 > sites, int width, int height)
Interface for entities that the server can send events to the clients
bool Equals(ClusterLocation anotherLocation)
void InitializeResources()
List< Item > Resources
Can be null unless initialized in constructor
ClusterLocation(VoronoiCell cell, GraphEdge edge, bool initializeResourceList=false)
List is initialized only when specified, otherwise will be null
bool Equals(VoronoiCell cell, GraphEdge edge)
bool IsEnclosedArea()
Caves, ruins, outposts and similar enclosed areas
readonly PositionType PositionType
InterestingPosition(Point position, PositionType positionType, Submarine submarine=null, bool isValid=true)
InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid=true)
InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid=true)
PathPoint WithResources(bool containsResources)
PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)
List< ClusterLocation > ClusterLocations
float NextClusterProbability
bool ShouldContainResources
List< Identifier > ResourceTags
List< Identifier > ResourceIds