Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs
2023-05-10 15:07:17 +03:00

4611 lines
211 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.RuinGeneration;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Voronoi2;
namespace Barotrauma
{
partial class Level : Entity, IServerSerializable
{
public enum PlacementType
{
Top, Bottom
}
public enum EventType
{
SingleDestructibleWall,
GlobalDestructibleWall
}
//all entities are disabled after they reach this depth
public const int MaxEntityDepth = -1000000;
public const float ShaftHeight = 1000.0f;
/// <summary>
/// How far outside the boundaries of the level the water current that pushes subs towards the level starts
/// </summary>
public const float OutsideBoundsCurrentMargin = 30000.0f;
/// <summary>
/// How far outside the boundaries of the level the strength of the current starts to increase exponentially
/// </summary>
public const float OutsideBoundsCurrentMarginExponential = 150000.0f;
/// <summary>
/// How far outside the boundaries of the level the current stops submarines entirely
/// </summary>
public const float OutsideBoundsCurrentHardLimit = 200000.0f;
/// <summary>
/// The level generator won't try to adjust the width of the main path above this limit.
/// </summary>
public const int MaxSubmarineWidth = 16000;
private static Level loaded;
public static Level Loaded
{
get { return loaded; }
private set
{
if (loaded == value) { return; }
loaded = value;
GameAnalyticsManager.SetCurrentLevel(loaded?.LevelData);
}
}
[Flags]
public enum PositionType
{
None = 0,
MainPath = 0x1,
SidePath = 0x2,
Cave = 0x4,
Ruin = 0x8,
Wreck = 0x10,
BeaconStation = 0x20,
Abyss = 0x40,
AbyssCave = 0x80,
Outpost = 0x100,
}
public struct InterestingPosition
{
public Point Position;
public readonly PositionType PositionType;
public bool IsValid;
public Submarine Submarine;
public Ruin Ruin;
public Cave Cave;
public InterestingPosition(Point position, PositionType positionType, Submarine submarine = null, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = submarine;
Ruin = null;
Cave = null;
}
public InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = null;
Ruin = ruin;
Cave = null;
}
public InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid = true)
{
Position = position;
PositionType = positionType;
IsValid = isValid;
Submarine = null;
Ruin = null;
Cave = cave;
}
}
public enum TunnelType
{
MainPath, SidePath, Cave
}
public class Tunnel
{
public readonly Tunnel ParentTunnel;
public readonly int MinWidth;
public readonly TunnelType Type;
public List<Point> Nodes
{
get;
private set;
}
public List<VoronoiCell> Cells
{
get;
private set;
}
public List<WayPoint> WayPoints
{
get;
private set;
}
public Tunnel(TunnelType type, List<Point> nodes, int minWidth, Tunnel parentTunnel)
{
Type = type;
MinWidth = minWidth;
ParentTunnel = parentTunnel;
Nodes = new List<Point>(nodes);
Cells = new List<VoronoiCell>();
WayPoints = new List<WayPoint>();
}
}
public class Cave
{
public Rectangle Area;
public readonly List<Tunnel> Tunnels = new List<Tunnel>();
public Point StartPos, EndPos;
public bool DisplayOnSonar;
public readonly CaveGenerationParams CaveGenerationParams;
public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos)
{
CaveGenerationParams = caveGenerationParams;
Area = area;
StartPos = startPos;
EndPos = endPos;
}
}
//how close the sub has to be to start/endposition to exit
public const float ExitDistance = 6000.0f;
public const int GridCellSize = 2000;
private List<VoronoiCell>[,] cellGrid;
private List<VoronoiCell> cells;
public Rectangle AbyssArea
{
get;
private set;
}
public int AbyssStart
{
get { return AbyssArea.Y + AbyssArea.Height; }
}
public int AbyssEnd
{
get { return AbyssArea.Y; }
}
public class AbyssIsland
{
public Rectangle Area;
public readonly List<VoronoiCell> Cells;
public AbyssIsland(Rectangle area, List<VoronoiCell> cells)
{
Debug.Assert(cells != null && cells.Any());
Area = area;
Cells = cells;
}
}
public List<AbyssIsland> AbyssIslands = new List<AbyssIsland>();
//TODO: make private
public List<double> siteCoordsX, siteCoordsY;
//TODO: make private
public List<(Point point, double distance)> distanceField;
private Point startPosition, endPosition;
private readonly Rectangle borders;
private List<Body> bodies;
private List<Point> bottomPositions;
//no need for frequent network updates, as currently the only thing that's synced
//are the slowly moving ice chunks that move in a very predictable way
const float NetworkUpdateInterval = 5.0f;
private float networkUpdateTimer;
public Vector2 StartPosition
{
get { return startPosition.ToVector2(); }
}
private Point startExitPosition;
public Vector2 StartExitPosition
{
get { return startExitPosition.ToVector2(); }
}
public Point Size
{
get { return LevelData.Size; }
}
public Vector2 EndPosition
{
get { return endPosition.ToVector2(); }
}
private Point endExitPosition;
public Vector2 EndExitPosition
{
get { return endExitPosition.ToVector2(); }
}
public int BottomPos
{
get;
private set;
}
public int SeaFloorTopPos
{
get;
private set;
}
public const float DefaultRealWorldCrushDepth = 3500.0f;
/// <summary>
/// The crush depth of a non-upgraded submarine in in-game coordinates. Note that this can be above the top of the level!
/// </summary>
public float CrushDepth
{
get
{
return LevelData.CrushDepth;
}
}
/// <summary>
/// The crush depth of a non-upgraded submarine in "real world units" (meters from the surface of Europa). Note that this can be above the top of the level!
/// </summary>
public float RealWorldCrushDepth
{
get
{
return LevelData.RealWorldCrushDepth;
}
}
public LevelWall SeaFloor { get; private set; }
public List<Ruin> Ruins { get; private set; }
public List<Submarine> Wrecks { get; private set; }
public Submarine BeaconStation { get; private set; }
private Sonar beaconSonar;
public List<LevelWall> ExtraWalls { get; private set; }
public List<LevelWall> UnsyncedExtraWalls { get; private set; }
public List<Tunnel> Tunnels { get; private set; } = new List<Tunnel>();
public List<Cave> Caves { get; private set; } = new List<Cave>();
public List<InterestingPosition> PositionsOfInterest { get; private set; }
public Submarine StartOutpost { get; private set; }
public Submarine EndOutpost { get; private set; }
private SubmarineInfo preSelectedStartOutpost;
private SubmarineInfo preSelectedEndOutpost;
public readonly LevelData LevelData;
/// <summary>
/// Random integers generated during the level generation. If these values differ between clients/server,
/// it means the levels aren't identical for some reason and there will most likely be major ID mismatches.
/// </summary>
public enum LevelGenStage
{
LevelGenParams,
Size,
GenStart,
TunnelGen,
AbyssGen,
CaveGen,
VoronoiGen,
VoronoiGen2,
VoronoiGen3,
Ruins,
FloatingIce,
LevelBodies,
IceSpires,
TopAndBottom,
PlaceLevelObjects,
GenerateItems,
Finish
}
private readonly Dictionary<LevelGenStage, int> equalityCheckValues = Enum.GetValues(typeof(LevelGenStage))
.Cast<LevelGenStage>()
.Select(k => (k, 0))
.ToDictionary();
public IReadOnlyDictionary<LevelGenStage, int> EqualityCheckValues => equalityCheckValues;
private void GenerateEqualityCheckValue(LevelGenStage stage)
{
equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient);
}
private void SetEqualityCheckValue(LevelGenStage stage, int value)
{
equalityCheckValues[stage] = value;
}
private void ClearEqualityCheckValues()
{
foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage)))
{
equalityCheckValues[stage] = 0;
}
}
public List<Entity> EntitiesBeforeGenerate { get; private set; } = new List<Entity>();
public int EntityCountBeforeGenerate { get; private set; }
public int EntityCountAfterGenerate { get; private set; }
public Body TopBarrier
{
get;
private set;
}
public Body BottomBarrier
{
get;
private set;
}
public LevelObjectManager LevelObjectManager { get; private set; }
public bool Generating { get; private set; }
public Location StartLocation { get; private set; }
public Location EndLocation { get; private set; }
public bool Mirrored
{
get;
private set;
}
public string Seed
{
get { return LevelData.Seed; }
}
public static float? ForcedDifficulty;
public float Difficulty
{
get { return ForcedDifficulty ?? LevelData.Difficulty; }
}
public LevelData.LevelType Type
{
get { return LevelData.Type; }
}
public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome;
/// <summary>
/// Is there a loaded level set and is it an outpost?
/// </summary>
public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost;
/// <summary>
/// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reputation into account.
/// </summary>
public static bool IsLoadedFriendlyOutpost =>
loaded?.Type == LevelData.LevelType.Outpost &&
(loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1);
public LevelGenerationParams GenerationParams
{
get { return LevelData.GenerationParams; }
}
public Color BackgroundTextureColor
{
get { return LevelData.GenerationParams.BackgroundTextureColor; }
}
public Color BackgroundColor
{
get { return LevelData.GenerationParams.BackgroundColor; }
}
public Color WallColor
{
get { return LevelData.GenerationParams.WallColor; }
}
private Level(LevelData levelData) : base(null, 0)
{
LevelData = levelData;
borders = new Rectangle(Point.Zero, levelData.Size);
}
public bool ShouldSpawnCrewInsideOutpost()
{
if (StartOutpost != null &&
Type == LevelData.LevelType.Outpost &&
(StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) &&
StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player))
{
return GameMain.GameSession.Campaign?.CurrentLocation is not { IsFactionHostile: true };
}
return false;
}
public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
{
Debug.Assert(levelData.Biome != null);
if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); }
if (levelData.Size.X <= 0) { throw new ArgumentException("Level width needs to be larger than zero."); }
if (levelData.Size.Y <= 0) { throw new ArgumentException("Level height needs to be larger than zero."); }
Level level = new Level(levelData)
{
preSelectedStartOutpost = startOutpost,
preSelectedEndOutpost = endOutpost
};
level.Generate(mirror, startLocation, endLocation);
return level;
}
private void Generate(bool mirror, Location startLocation, Location endLocation)
{
Loaded?.Remove();
Loaded = this;
Generating = true;
#if CLIENT
Debug.Assert(GenerationParams.Identifier != "coldcavernstutorial" || GameMain.GameSession?.GameMode == null || GameMain.GameSession.GameMode is TutorialMode);
#endif
Debug.Assert(GenerationParams.AnyBiomeAllowed || GenerationParams.AllowedBiomeIdentifiers.Contains(LevelData.Biome.Identifier));
DebugConsole.NewMessage("Level identifier: " + GenerationParams.Identifier);
ClearEqualityCheckValues();
EntitiesBeforeGenerate = GetEntities().ToList();
EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count();
StartLocation = startLocation;
EndLocation = endLocation;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
GenerateEqualityCheckValue(LevelGenStage.GenStart);
SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier));
SetEqualityCheckValue(LevelGenStage.Size, borders.Width ^ borders.Height << 16);
GenerateEqualityCheckValue(LevelGenStage.TunnelGen);
LevelObjectManager = new LevelObjectManager();
if (Type == LevelData.LevelType.Outpost) { mirror = false; }
Mirrored = mirror;
#if CLIENT
if (backgroundCreatureManager == null)
{
var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
}
#endif
Stopwatch sw = new Stopwatch();
sw.Start();
PositionsOfInterest = new List<InterestingPosition>();
ExtraWalls = new List<LevelWall>();
UnsyncedExtraWalls = new List<LevelWall>();
bodies = new List<Body>();
List<Vector2> sites = new List<Vector2>();
Voronoi voronoi = new Voronoi(1.0);
#if CLIENT
renderer = new LevelRenderer(this);
#endif
SeaFloorTopPos = GenerationParams.SeaFloorDepth + GenerationParams.MountainHeightMax + GenerationParams.SeaFloorVariance;
int minMainPathWidth = Math.Min(GenerationParams.MinTunnelRadius, MaxSubmarineWidth);
int minWidth = 500;
if (Submarine.MainSub != null)
{
Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders();
dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f);
minWidth = Math.Max(dockedSubBorders.Width, dockedSubBorders.Height);
minMainPathWidth = Math.Max(minMainPathWidth, minWidth);
minMainPathWidth = Math.Min(minMainPathWidth, MaxSubmarineWidth);
}
minMainPathWidth = Math.Min(minMainPathWidth, borders.Width / 5);
LevelData.MinMainPathWidth = minMainPathWidth;
Rectangle pathBorders = borders;
pathBorders.Inflate(
-Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5),
-Math.Min(minMainPathWidth * 2, borders.Height / 5));
if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); }
if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); }
startPosition = new Point(
(int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.StartPosition.X),
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.StartPosition.Y));
startExitPosition = new Point(startPosition.X, borders.Bottom);
endPosition = new Point(
(int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.EndPosition.X),
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
endExitPosition = new Point(endPosition.X, borders.Bottom);
GenerateEqualityCheckValue(LevelGenStage.TunnelGen);
//----------------------------------------------------------------------------------
//generate the initial nodes for the main path and smaller tunnels
//----------------------------------------------------------------------------------
Tunnel mainPath = new Tunnel(
TunnelType.MainPath,
GeneratePathNodes(startPosition, endPosition, pathBorders, null, GenerationParams.MainPathVariance),
minMainPathWidth, parentTunnel: null);
Tunnels.Add(mainPath);
Tunnel startPath = null, endPath = null, endHole = null;
if (GenerationParams.StartPosition.Y < 0.5f && (Mirrored ? !HasEndOutpost() : !HasStartOutpost()))
{
startPath = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startExitPosition, startPosition },
minWidth, parentTunnel: mainPath);
Tunnels.Add(startPath);
}
else
{
startExitPosition = startPosition;
}
if (GenerationParams.EndPosition.Y < 0.5f && (Mirrored ? !HasStartOutpost() : !HasEndOutpost()))
{
endPath = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition },
minWidth, parentTunnel: mainPath);
Tunnels.Add(endPath);
}
else
{
endExitPosition = endPosition;
}
if (GenerationParams.CreateHoleNextToEnd)
{
if (Mirrored)
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startPosition, startExitPosition, new Point(0, Size.Y) },
minWidth, parentTunnel: mainPath);
}
else
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition, Size },
minWidth, parentTunnel: mainPath);
}
Tunnels.Add(endHole);
}
//create a tunnel from the lowest point in the main path to the abyss
//to ensure there's a way to the abyss in all levels
Tunnel abyssTunnel = null;
if (GenerationParams.CreateHoleToAbyss)
{
Point lowestPoint = mainPath.Nodes.First();
foreach (var pathNode in mainPath.Nodes)
{
if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; }
}
abyssTunnel = new Tunnel(
TunnelType.SidePath,
new List<Point>() { lowestPoint, new Point(lowestPoint.X, 0) },
minWidth, parentTunnel: mainPath);
Tunnels.Add(abyssTunnel);
}
int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < sideTunnelCount; j++)
{
if (mainPath.Nodes.Count < 4) { break; }
var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)];
if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; }
Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)];
Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)];
var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance);
//make sure the path is wide enough to pass through
int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient);
Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
}
CalculateTunnelDistanceField(null);
GenerateSeaFloorPositions();
GenerateEqualityCheckValue(LevelGenStage.AbyssGen);
GenerateAbyssArea();
GenerateEqualityCheckValue(LevelGenStage.CaveGen);
GenerateCaves(mainPath);
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
//----------------------------------------------------------------------------------
//generate voronoi sites
//----------------------------------------------------------------------------------
Point siteInterval = GenerationParams.VoronoiSiteInterval;
int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
Point siteVariance = GenerationParams.VoronoiSiteVariance;
siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
const int caveSiteInterval = 500;
for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X)
{
for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y)
{
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient);
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient);
bool closeToTunnel = false;
bool closeToCave = false;
foreach (Tunnel tunnel in Tunnels)
{
float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y));
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; }
if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; }
if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; }
if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; }
double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (Math.Sqrt(tunnelDistSqr) < minDist)
{
closeToTunnel = true;
//tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY));
if (tunnel.Type == TunnelType.Cave)
{
closeToCave = true;
}
break;
}
}
}
if (!closeToTunnel)
{
//make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry
if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
}
if (!TooCloseToOtherSites(siteX, siteY))
{
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
}
if (closeToCave)
{
for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
{
for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
{
siteCoordsX.Add(caveSiteX);
siteCoordsY.Add(caveSiteY);
}
}
}
}
}
}
bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f)
{
float minDistanceSqr = minDistance * minDistance;
for (int i = 0; i < siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
{
return true;
}
}
return false;
}
for (int i = 0; i < siteCoordsX.Count; i++)
{
Debug.Assert(
siteCoordsX[i] > 0 || siteCoordsY[i] > 0,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
Debug.Assert(
siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height,
$"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})");
for (int j = i + 1; j < siteCoordsX.Count; j++)
{
Debug.Assert(
MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f,
"Potential error in level generation: two voronoi sites are extremely close to each other.");
}
}
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2);
//----------------------------------------------------------------------------------
// construct the voronoi graph and cells
//----------------------------------------------------------------------------------
Stopwatch sw2 = new Stopwatch();
sw2.Start();
Debug.Assert(siteCoordsX.Count == siteCoordsY.Count);
List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height);
Debug.WriteLine("MakeVoronoiGraph: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
//construct voronoi cells based on the graph edges
cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid);
GenerateAbyssGeometry();
GenerateAbyssPositions();
Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
//----------------------------------------------------------------------------------
// generate a path through the tunnel nodes
//----------------------------------------------------------------------------------
List<VoronoiCell> pathCells = new List<VoronoiCell>();
foreach (Tunnel tunnel in Tunnels)
{
CaveGenerator.GeneratePath(tunnel, this);
if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath)
{
if (tunnel != startPath && tunnel != endPath && tunnel != endHole)
{
var distinctCells = tunnel.Cells.Distinct().ToList();
for (int i = 2; i < distinctCells.Count; i += 3)
{
PositionsOfInterest.Add(new InterestingPosition(
new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y),
tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath,
Caves.Find(cave => cave.Tunnels.Contains(tunnel))));
}
}
}
bool connectToParentTunnel = tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave;
GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel : null);
EnlargePath(tunnel.Cells, tunnel.MinWidth);
foreach (var pathCell in tunnel.Cells)
{
MarkEdges(pathCell, tunnel.Type);
if (!pathCells.Contains(pathCell))
{
pathCells.Add(pathCell);
}
}
static void MarkEdges(VoronoiCell cell, TunnelType tunnelType)
{
foreach (GraphEdge edge in cell.Edges)
{
switch (tunnelType)
{
case TunnelType.MainPath:
edge.NextToMainPath = true;
break;
case TunnelType.SidePath:
edge.NextToSidePath = true;
break;
case TunnelType.Cave:
edge.NextToCave = true;
break;
}
}
}
}
var potentialIslands = new List<VoronoiCell>();
foreach (var cell in pathCells)
{
if (GetDistToTunnel(cell.Center, mainPath) < minMainPathWidth ||
(startPath != null && GetDistToTunnel(cell.Center, startPath) < minMainPathWidth) ||
(endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) ||
(endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; }
if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; }
if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; }
potentialIslands.Add(cell);
}
for (int i = 0; i < GenerationParams.IslandCount; i++)
{
if (potentialIslands.Count == 0) { break; }
var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient);
island.CellType = CellType.Solid;
island.Island = true;
pathCells.Remove(island);
}
foreach (InterestingPosition positionOfInterest in PositionsOfInterest)
{
WayPoint wayPoint = new WayPoint(
positionOfInterest.Position.ToVector2(),
SpawnType.Enemy,
submarine: null);
}
startPosition.X = (int)pathCells[0].Site.Coord.X;
startExitPosition.X = startPosition.X;
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3);
//----------------------------------------------------------------------------------
// remove unnecessary cells and create some holes at the bottom of the level
//----------------------------------------------------------------------------------
if (GenerationParams.NoLevelGeometry)
{
cells.ForEach(c => c.CellType = CellType.Removed);
cells.Clear();
}
cells = cells.Except(pathCells).ToList();
//remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad
cells.ForEachMod(c =>
{
if (c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y, Size.Y) && e.AdjacentCell(c) == null))
{
c.CellType = CellType.Removed;
cells.Remove(c);
}
});
int xPadding = borders.Width / 5;
pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minMainPathWidth));
foreach (VoronoiCell cell in cells)
{
if (cell.Site.Coord.Y < borders.Height / 2) { continue; }
cell.Edges.ForEach(e => e.OutsideLevel = true);
}
foreach (AbyssIsland abyssIsland in AbyssIslands)
{
cells.AddRange(abyssIsland.Cells);
}
List<Point> ruinPositions = new List<Point>();
int ruinCount = GenerationParams.RuinCount;
if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false)
{
ruinCount = Math.Max(ruinCount, 1);
}
for (int i = 0; i < ruinCount; i++)
{
Point ruinSize = new Point(5000);
int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
int limitRight = Math.Min(endPosition.X, Size.X - ruinSize.X / 2);
Rectangle limits = new Rectangle(limitLeft, ruinSize.Y, limitRight - limitLeft, Size.Y - ruinSize.Y);
Debug.Assert(limits.Width > 0);
Debug.Assert(limits.Height > 0);
ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, limits: limits));
CalculateTunnelDistanceField(ruinPositions);
}
//----------------------------------------------------------------------------------
// initialize the cells that are still left and insert them into the cell grid
//----------------------------------------------------------------------------------
foreach (VoronoiCell cell in pathCells)
{
cell.Edges.ForEach(e => e.OutsideLevel = false);
cell.CellType = CellType.Path;
cells.Remove(cell);
}
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Clear();
}
}
//----------------------------------------------------------------------------------
// mirror if needed
//----------------------------------------------------------------------------------
if (mirror)
{
HashSet<GraphEdge> mirroredEdges = new HashSet<GraphEdge>();
HashSet<Site> mirroredSites = new HashSet<Site>();
List<VoronoiCell> allCells = new List<VoronoiCell>(cells);
allCells.AddRange(pathCells);
foreach (VoronoiCell cell in allCells)
{
foreach (GraphEdge edge in cell.Edges)
{
if (mirroredEdges.Contains(edge)) { continue; }
edge.Point1.X = borders.Width - edge.Point1.X;
edge.Point2.X = borders.Width - edge.Point2.X;
if (edge.Site1 != null && !mirroredSites.Contains(edge.Site1))
{
//make sure that sites right at the edge of a grid cell end up in the same cell as in the non-mirrored level
if (edge.Site1.Coord.X % GridCellSize < 1.0f &&
edge.Site1.Coord.X % GridCellSize >= 0.0f) { edge.Site1.Coord.X += 1.0f; }
edge.Site1.Coord.X = borders.Width - edge.Site1.Coord.X;
mirroredSites.Add(edge.Site1);
}
if (edge.Site2 != null && !mirroredSites.Contains(edge.Site2))
{
if (edge.Site2.Coord.X % GridCellSize < 1.0f &&
edge.Site2.Coord.X % GridCellSize >= 0.0f) { edge.Site2.Coord.X += 1.0f; }
edge.Site2.Coord.X = borders.Width - edge.Site2.Coord.X;
mirroredSites.Add(edge.Site2);
}
mirroredEdges.Add(edge);
}
}
foreach (AbyssIsland island in AbyssIslands)
{
island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height);
foreach (var cell in island.Cells)
{
if (!mirroredSites.Contains(cell.Site))
{
if (cell.Site.Coord.X % GridCellSize < 1.0f &&
cell.Site.Coord.X % GridCellSize >= 0.0f) { cell.Site.Coord.X += 1.0f; }
cell.Site.Coord.X = borders.Width - cell.Site.Coord.X;
mirroredSites.Add(cell.Site);
}
}
}
for (int i = 0; i < ruinPositions.Count; i++)
{
ruinPositions[i] = new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y);
}
foreach (Cave cave in Caves)
{
cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height);
cave.StartPos = new Point(borders.Width - cave.StartPos.X, cave.StartPos.Y);
cave.EndPos = new Point(borders.Width - cave.EndPos.X, cave.EndPos.Y);
}
foreach (Tunnel tunnel in Tunnels)
{
for (int i = 0; i < tunnel.Nodes.Count; i++)
{
tunnel.Nodes[i] = new Point(borders.Width - tunnel.Nodes[i].X, tunnel.Nodes[i].Y);
}
}
for (int i = 0; i < PositionsOfInterest.Count; i++)
{
PositionsOfInterest[i] = new InterestingPosition(
new Point(borders.Width - PositionsOfInterest[i].Position.X, PositionsOfInterest[i].Position.Y),
PositionsOfInterest[i].PositionType)
{
Submarine = PositionsOfInterest[i].Submarine,
Cave = PositionsOfInterest[i].Cave,
Ruin = PositionsOfInterest[i].Ruin,
};
}
foreach (WayPoint waypoint in WayPoint.WayPointList)
{
if (waypoint.Submarine != null) continue;
waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f));
}
for (int i = 0; i < bottomPositions.Count; i++)
{
bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y);
}
bottomPositions.Reverse();
startPosition.X = borders.Width - startPosition.X;
endPosition.X = borders.Width - endPosition.X;
startExitPosition.X = borders.Width - startExitPosition.X;
endExitPosition.X = borders.Width - endExitPosition.X;
CalculateTunnelDistanceField(ruinPositions);
}
foreach (VoronoiCell cell in cells)
{
int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize);
x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1);
int y = (int)Math.Floor(cell.Site.Coord.Y / GridCellSize);
y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1);
cellGrid[x, y].Add(cell);
}
float destructibleWallRatio = MathHelper.Lerp(0.2f, 1.0f, LevelData.Difficulty / 100.0f);
foreach (Cave cave in Caves)
{
if (cave.Area.Y > 0)
{
List<VoronoiCell> cavePathCells = CreatePathToClosestTunnel(cave.StartPos);
var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type != TunnelType.Cave);
WayPoint prevWp = mainTunnel.WayPoints.First();
if (prevWp != null)
{
for (int i = 0; i < cavePathCells.Count; i++)
{
var connectingEdge = i > 0 ? cavePathCells[i].Edges.Find(e => e.AdjacentCell(cavePathCells[i]) == cavePathCells[i - 1]) : null;
if (connectingEdge != null)
{
var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, edgeWayPoint, 500.0f);
prevWp = edgeWayPoint;
}
var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, newWaypoint, 500.0f);
prevWp = newWaypoint;
}
var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints);
ConnectWaypoints(prevWp, closestPathPoint, 500.0f);
}
}
List<VoronoiCell> caveCells = new List<VoronoiCell>();
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
foreach (var caveCell in caveCells)
{
if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
{
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
if (chunk != null)
{
chunk.Body.BodyType = BodyType.Static;
ExtraWalls.Add(chunk);
}
}
}
}
GenerateEqualityCheckValue(LevelGenStage.Ruins);
//----------------------------------------------------------------------------------
// create some ruins
//----------------------------------------------------------------------------------
Ruins = new List<Ruin>();
for (int i = 0; i < ruinPositions.Count; i++)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i);
GenerateRuin(ruinPositions[i], mirror);
}
GenerateEqualityCheckValue(LevelGenStage.FloatingIce);
//----------------------------------------------------------------------------------
// create floating ice chunks
//----------------------------------------------------------------------------------
if (GenerationParams.FloatingIceChunkCount > 0)
{
List<Point> iceChunkPositions = new List<Point>();
foreach (InterestingPosition pos in PositionsOfInterest)
{
if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; }
if (pos.Position.X < pathBorders.X + minMainPathWidth || pos.Position.X > pathBorders.Right - minMainPathWidth) { continue; }
if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; }
if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; }
iceChunkPositions.Add(pos.Position);
}
for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++)
{
if (iceChunkPositions.Count == 0) { break; }
Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)];
float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient);
var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f);
var chunk = CreateIceChunk(vertices, selectedPos.ToVector2());
chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f);
chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient);
ExtraWalls.Add(chunk);
iceChunkPositions.Remove(selectedPos);
}
}
GenerateEqualityCheckValue(LevelGenStage.LevelBodies);
//----------------------------------------------------------------------------------
// generate the bodies and rendered triangles of the cells
//----------------------------------------------------------------------------------
foreach (VoronoiCell cell in cells)
{
foreach (GraphEdge ge in cell.Edges)
{
VoronoiCell adjacentCell = ge.AdjacentCell(cell);
ge.IsSolid = adjacentCell == null || !cells.Contains(adjacentCell);
}
}
List<VoronoiCell> cellsWithBody = new List<VoronoiCell>(cells);
if (GenerationParams.CellRoundingAmount > 0.01f || GenerationParams.CellIrregularity > 0.01f)
{
foreach (VoronoiCell cell in cellsWithBody)
{
CaveGenerator.RoundCell(cell,
minEdgeLength: GenerationParams.CellSubdivisionLength,
roundingAmount: GenerationParams.CellRoundingAmount,
irregularity: GenerationParams.CellIrregularity);
}
}
#if CLIENT
List<(List<VoronoiCell> cells, Cave parentCave)> cellBatches = new List<(List<VoronoiCell>, Cave)>
{
(cellsWithBody.ToList(), null)
};
foreach (Cave cave in Caves)
{
(List<VoronoiCell> cells, Cave parentCave) newCellBatch = (new List<VoronoiCell>(), cave);
foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells))
{
foreach (var edge in caveCell.Edges)
{
if (!edge.NextToCave) { continue; }
if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell1))
{
Debug.Assert(cellsWithBody.Contains(edge.Cell1));
cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell1));
newCellBatch.cells.Add(edge.Cell1);
}
if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell2))
{
Debug.Assert(cellsWithBody.Contains(edge.Cell2));
cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell2));
newCellBatch.cells.Add(edge.Cell2);
}
}
}
if (newCellBatch.cells.Any())
{
cellBatches.Add(newCellBatch);
}
}
cellBatches.RemoveAll(cb => !cb.cells.Any());
int totalCellsInBatches = cellBatches.Sum(cb => cb.cells.Count);
Debug.Assert(cellsWithBody.Count == totalCellsInBatches);
List<List<Vector2[]>> triangleLists = new List<List<Vector2[]>>();
foreach ((List<VoronoiCell> cells, Cave cave) cellBatch in cellBatches)
{
bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.cells, this, out List<Vector2[]> triangles));
triangleLists.Add(triangles);
}
#else
bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List<Vector2[]> triangles));
#endif
foreach (VoronoiCell cell in cells)
{
CompareCCW compare = new CompareCCW(cell.Center);
foreach (GraphEdge edge in cell.Edges)
{
//remove references to cells that we failed to generate a body for
if (edge.Cell1 != null && edge.Cell1.Body == null && edge.Cell1.CellType != CellType.Empty) { edge.Cell1 = null; }
if (edge.Cell2 != null && edge.Cell2.Body == null && edge.Cell2.CellType != CellType.Empty) { edge.Cell2 = null; }
//make the order of the points CCW
if (compare.Compare(edge.Point1, edge.Point2) == -1)
{
var temp = edge.Point1;
edge.Point1 = edge.Point2;
edge.Point2 = temp;
}
}
}
#if CLIENT
Debug.Assert(triangleLists.Count == cellBatches.Count);
for (int i = 0; i < triangleLists.Count; i++)
{
renderer.SetVertices(
CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(),
CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(),
cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture,
cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture,
GenerationParams.WallColor);
}
#endif
GenerateEqualityCheckValue(LevelGenStage.IceSpires);
//----------------------------------------------------------------------------------
// create ice spires
//----------------------------------------------------------------------------------
List<GraphEdge> usedSpireEdges = new List<GraphEdge>();
for (int i = 0; i < GenerationParams.IceSpireCount; i++)
{
var spire = CreateIceSpire(usedSpireEdges);
if (spire != null) { ExtraWalls.Add(spire); };
}
//----------------------------------------------------------------------------------
// connect side paths and cave branches to their parents
//----------------------------------------------------------------------------------
foreach (Ruin ruin in Ruins)
{
GenerateRuinWayPoints(ruin);
}
foreach (Tunnel tunnel in Tunnels)
{
if (tunnel.ParentTunnel == null) { continue; }
if (tunnel.Type == TunnelType.Cave && tunnel.ParentTunnel == mainPath) { continue; }
ConnectWaypoints(tunnel, tunnel.ParentTunnel);
}
//----------------------------------------------------------------------------------
// create outposts at the start and end of the level
//----------------------------------------------------------------------------------
CreateOutposts();
GenerateEqualityCheckValue(LevelGenStage.TopAndBottom);
//----------------------------------------------------------------------------------
// top barrier & sea floor
//----------------------------------------------------------------------------------
TopBarrier = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)),
ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0)));
TopBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, borders.Height)), 0.0f);
TopBarrier.BodyType = BodyType.Static;
TopBarrier.CollisionCategories = Physics.CollisionLevel;
bodies.Add(TopBarrier);
GenerateSeaFloor();
if (mirror)
{
Point tempP = startPosition;
startPosition = endPosition;
endPosition = tempP;
tempP = startExitPosition;
startExitPosition = endExitPosition;
endExitPosition = tempP;
}
if (StartOutpost != null)
{
startExitPosition = StartOutpost.WorldPosition.ToPoint();
startPosition = startExitPosition;
}
if (EndOutpost != null)
{
endExitPosition = EndOutpost.WorldPosition.ToPoint();
endPosition = endExitPosition;
}
CreateWrecks();
CreateBeaconStation();
GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects);
LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount);
GenerateEqualityCheckValue(LevelGenStage.GenerateItems);
GenerateItems();
GenerateEqualityCheckValue(LevelGenStage.Finish);
#if CLIENT
backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount);
#endif
foreach (VoronoiCell cell in cells)
{
foreach (GraphEdge edge in cell.Edges)
{
//edge.Cell1 = null;
//edge.Cell2 = null;
edge.Site1 = null;
edge.Site2 = null;
}
}
//initialize MapEntities that aren't in any sub (e.g. items inside ruins)
MapEntity.MapLoaded(MapEntity.mapEntityList.FindAll(me => me.Submarine == null), false);
Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
Debug.WriteLine("**********************************************************************************");
Debug.WriteLine("Generated a map with " + siteCoordsX.Count + " sites in " + sw.ElapsedMilliseconds + " ms");
Debug.WriteLine("Seed: " + Seed);
Debug.WriteLine("**********************************************************************************");
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage("Generated level with the seed " + Seed + " (type: " + GenerationParams.Identifier + ")", Color.White);
}
EntityCountAfterGenerate = GetEntities().Count();
#if SERVER
if (GameMain.Server.EntityEventManager.Events.Count() > 0)
{
DebugConsole.NewMessage("WARNING: Entity events have been created during level generation. Events should not be created until the round is fully initialized.");
}
GameMain.Server.EntityEventManager.Clear();
#endif
//assign an ID to make entity events work
//ID = FindFreeID();
Generating = false;
}
private List<Point> GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance)
{
List<Point> pathNodes = new List<Point> { startPosition };
Point nodeInterval = GenerationParams.MainPathNodeIntervalRange;
for (int x = startPosition.X + nodeInterval.X;
x < endPosition.X - nodeInterval.X;
x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient))
{
Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient));
//allow placing the 2nd main path node at any height regardless of variance
//(otherwise low variance will always make the main path go through the upper part of the level)
if (pathNodes.Count > 2 || parentTunnel != null)
{
nodePos.Y = (int)MathHelper.Clamp(
nodePos.Y,
pathNodes.Last().Y - pathBorders.Height * variance * 0.5f,
pathNodes.Last().Y + pathBorders.Height * variance * 0.5f);
}
if (pathNodes.Count == 1)
{
//if the path starts below the center of the level, head up and vice versa
//to utilize as much of the vertical space as possible
nodePos.Y = (int)(startPosition.Y + Math.Abs(nodePos.Y - startPosition.Y) * -Math.Sign(nodePos.Y - pathBorders.Center.Y));
nodePos.Y = MathHelper.Clamp(nodePos.Y, pathBorders.Y, pathBorders.Bottom);
}
//prevent intersections with other tunnels
foreach (Tunnel tunnel in Tunnels)
{
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
Point node1 = tunnel.Nodes[i - 1];
Point node2 = tunnel.Nodes[i];
if (node1.X >= nodePos.X) { continue; }
if (node2.X <= pathNodes.Last().X) { continue; }
if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) { continue; }
if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth &&
!MathUtils.LinesIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2()))
{
continue;
}
if (nodePos.Y < pathNodes.Last().Y)
{
nodePos.Y = Math.Min(Math.Max(node1.Y, node2.Y) + tunnel.MinWidth * 2, pathBorders.Bottom);
}
else
{
nodePos.Y = Math.Max(Math.Min(node1.Y, node2.Y) - tunnel.MinWidth * 2, pathBorders.Y);
}
break;
}
}
pathNodes.Add(nodePos);
}
if (pathNodes.Count == 1)
{
pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y));
}
pathNodes.Add(endPosition);
return pathNodes;
}
private List<VoronoiCell> CreateHoles(float holeProbability, Rectangle limits, int submarineSize)
{
List<VoronoiCell> toBeRemoved = new List<VoronoiCell>();
foreach (VoronoiCell cell in cells)
{
if (cell.Edges.Any(e => e.NextToCave)) { continue; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) { continue; }
if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; }
float closestDist = 0.0f;
Point? closestTunnelNode = null;
foreach (Tunnel tunnel in Tunnels)
{
foreach (Point node in tunnel.Nodes)
{
float dist = Math.Abs(cell.Center.X - node.X);
if (closestTunnelNode == null || dist < closestDist)
{
closestDist = dist;
closestTunnelNode = node;
}
}
}
if (closestTunnelNode != null && closestTunnelNode.Value.Y < cell.Center.Y) { continue; }
toBeRemoved.Add(cell);
}
return toBeRemoved;
}
private void EnlargePath(List<VoronoiCell> pathCells, float minWidth)
{
if (minWidth <= 0.0f) { return; }
List<VoronoiCell> removedCells = GetTooCloseCells(pathCells, minWidth);
foreach (VoronoiCell removedCell in removedCells)
{
if (removedCell.CellType == CellType.Path) { continue; }
pathCells.Add(removedCell);
removedCell.CellType = CellType.Path;
}
}
private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel)
{
if (tunnel.Cells.Count == 0) { return; }
List<WayPoint> wayPoints = new List<WayPoint>();
WayPoint prevWayPoint = null;
for (int i = 0; i < tunnel.Cells.Count; i++)
{
tunnel.Cells[i].CellType = CellType.Path;
var newWaypoint = new WayPoint(new Rectangle((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Center.Y, 10, 10), null)
{
Tunnel = tunnel
};
wayPoints.Add(newWaypoint);
if (prevWayPoint != null)
{
bool solidCellBetween = false;
foreach (GraphEdge edge in tunnel.Cells[i].Edges)
{
if (edge.AdjacentCell(tunnel.Cells[i])?.CellType == CellType.Solid &&
MathUtils.LinesIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2))
{
solidCellBetween = true;
break;
}
}
if (solidCellBetween)
{
//something between the previous waypoint and this one
// -> find the edge that connects the cells and place a waypoint there, instead of connecting the centers of the cells directly
var edgeBetweenCells = tunnel.Cells[i].Edges.Find(e => e.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[i - 1]);
if (edgeBetweenCells != null)
{
var edgeWaypoint = new WayPoint(new Rectangle((int)edgeBetweenCells.Center.X, (int)edgeBetweenCells.Center.Y, 10, 10), null)
{
Tunnel = tunnel
};
prevWayPoint.ConnectTo(edgeWaypoint);
prevWayPoint = edgeWaypoint;
}
}
prevWayPoint.ConnectTo(newWaypoint);
//look back at the tunnel cells before the previous one, and see if the current cell shares edges with them
//= if we can "skip" from cell #1 to cell #3, create a waypoint between them.
//Fixes there sometimes not being a path past a destructible ice chunk even if there's space to go past it.
for (int j = i - 2; j > 0 && j > i - 5; j--)
{
foreach (GraphEdge edge in tunnel.Cells[i].Edges)
{
if (Vector2.DistanceSquared(edge.Point1, edge.Point2) < 30.0f * 30.0f) { continue; }
if (!edge.IsSolid && edge.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[j])
{
var edgeWaypoint = new WayPoint(new Rectangle((int)edge.Center.X, (int)edge.Center.Y, 10, 10), null)
{
Tunnel = tunnel
};
wayPoints[j].ConnectTo(edgeWaypoint);
edgeWaypoint.ConnectTo(newWaypoint);
break;
}
}
}
}
prevWayPoint = newWaypoint;
}
tunnel.WayPoints.AddRange(wayPoints);
//connect to the tunnel we're branching off from
if (parentTunnel != null)
{
var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel);
if (parentStart != null)
{
wayPoints.First().ConnectTo(parentStart);
}
if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave)
{
var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel);
if (parentEnd != null)
{
wayPoints.Last().ConnectTo(parentEnd);
}
}
}
}
private void ConnectWaypoints(Tunnel tunnel, Tunnel parentTunnel)
{
foreach (WayPoint wayPoint in tunnel.WayPoints)
{
var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel);
if (closestWaypoint == null) { continue; }
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(wayPoint.WorldPosition),
ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null)
{
float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f;
ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel);
}
}
}
private List<WayPoint> ConnectWaypoints(WayPoint wp1, WayPoint wp2, float interval)
{
List<WayPoint> newWaypoints = new List<WayPoint>();
Vector2 diff = wp2.WorldPosition - wp1.WorldPosition;
float dist = diff.Length();
WayPoint prevWaypoint = wp1;
for (float x = interval; x < dist - interval; x += interval)
{
var newWaypoint = new WayPoint(wp1.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null);
prevWaypoint.ConnectTo(newWaypoint);
prevWaypoint = newWaypoint;
newWaypoints.Add(newWaypoint);
}
prevWaypoint.ConnectTo(wp2);
return newWaypoints;
}
private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel)
{
return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints);
}
private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable<WayPoint> waypoints, Func<WayPoint, bool> filter = null)
{
float closestDist = float.PositiveInfinity;
WayPoint closestWayPoint = null;
foreach (WayPoint otherWayPoint in waypoints)
{
float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition);
if (dist < closestDist)
{
if (filter != null)
{
if (!filter(otherWayPoint)) { continue; }
}
closestDist = dist;
closestWayPoint = otherWayPoint;
}
}
return closestWayPoint;
}
private List<VoronoiCell> GetTooCloseCells(List<VoronoiCell> emptyCells, float minDistance)
{
List<VoronoiCell> tooCloseCells = new List<VoronoiCell>();
if (minDistance <= 0.0f) { return tooCloseCells; }
foreach (var cell in emptyCells.Distinct())
{
foreach (var tooCloseCell in GetTooCloseCells(cell.Center, minDistance))
{
if (!tooCloseCells.Contains(tooCloseCell))
{
tooCloseCells.Add(tooCloseCell);
}
}
}
return tooCloseCells;
}
public List<VoronoiCell> GetTooCloseCells(Vector2 position, float minDistance)
{
HashSet<VoronoiCell> tooCloseCells = new HashSet<VoronoiCell>();
var closeCells = GetCells(position, searchDepth: Math.Max((int)Math.Ceiling(minDistance / GridCellSize), 3));
float minDistSqr = minDistance * minDistance;
foreach (VoronoiCell cell in closeCells)
{
bool tooClose = false;
if (cell.IsPointInsideAABB(position, margin: minDistance))
{
tooClose = true;
}
else
{
foreach (GraphEdge edge in cell.Edges)
{
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr ||
MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr)
{
tooClose = true;
break;
}
}
}
if (tooClose) { tooCloseCells.Add(cell); }
}
return tooCloseCells.ToList();
}
private void GenerateAbyssPositions()
{
int count = 10;
for (int i = 0; i < count; i++)
{
float xPos = MathHelper.Lerp(borders.X, borders.Right, i / (float)(count - 1));
float seaFloorPos = GetBottomPosition(xPos).Y;
//above the bottom of the level = can't place a point here
if (seaFloorPos > AbyssStart) { continue; }
float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient));
foreach (var abyssIsland in AbyssIslands)
{
if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos)))
{
xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
}
}
PositionsOfInterest.Add(new InterestingPosition(new Point((int)xPos, (int)yPos), PositionType.Abyss));
}
}
private void GenerateAbyssArea()
{
int abyssStartY = borders.Y - 5000;
int abyssEndY = Math.Max(abyssStartY - 100000, BottomPos + 1000);
int abyssHeight = abyssStartY - abyssEndY;
if (abyssHeight < 0)
{
abyssStartY = borders.Y;
abyssEndY = BottomPos;
if (abyssStartY - abyssEndY < 1000)
{
#if DEBUG
DebugConsole.ThrowError("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
#else
DebugConsole.AddWarning("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper.");
#endif
}
}
else if (abyssHeight > 30000)
{
//if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth
//but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth)
if (abyssEndY + CrushDepth < 0 && abyssStartY > -CrushDepth)
{
abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2);
}
if (abyssStartY - abyssEndY < 10000)
{
abyssStartY = borders.Y;
}
}
AbyssArea = new Rectangle(borders.X, abyssEndY, borders.Width, abyssStartY - abyssEndY);
}
private void GenerateAbyssGeometry()
{
//TODO: expose island parameters
Voronoi voronoi = new Voronoi(1.0);
Point siteInterval = new Point(500, 500);
Point siteVariance = new Point(200, 200);
Point islandSize = Vector2.Lerp(
GenerationParams.AbyssIslandSizeMin.ToVector2(),
GenerationParams.AbyssIslandSizeMax.ToVector2(),
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint();
if (AbyssArea.Height < islandSize.Y) { return; }
int createdCaves = 0;
int islandCount = GenerationParams.AbyssIslandCount;
for (int i = 0; i < islandCount; i++)
{
Point islandPosition = Point.Zero;
Rectangle islandArea = new Rectangle(islandPosition, islandSize);
//prevent overlaps
int tries = 0;
const int MaxTries = 20;
do
{
islandPosition = new Point(
Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient),
Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient));
//move the island above the sea floor geometry
islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500);
islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X + islandArea.Width).Y + 500);
islandArea.Location = islandPosition;
tries++;
} while ((AbyssIslands.Any(island => island.Area.Intersects(islandArea)) || islandArea.Bottom > AbyssArea.Bottom) && tries < MaxTries);
if (tries >= MaxTries)
{
break;
}
bool createCave =
//force at least one abyss cave
(i == islandCount - 1 && createdCaves == 0) ||
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < GenerationParams.AbyssIslandCaveProbability;
if (!createCave)
{
//just create a chunk with no cave
float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f;
var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance);
Vector2 position = islandArea.Center.ToVector2();
for (int j = 0; j < vertices.Count; j++)
{
vertices[j] += position;
}
var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this, createBody: false);
AbyssIslands.Add(new AbyssIsland(islandArea, newChunk.Cells));
continue;
}
var siteCoordsX = new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
var siteCoordsY = new List<double>((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y));
for (int x = islandArea.X; x < islandArea.Right; x += siteInterval.X)
{
for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y)
{
siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient));
siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient));
}
}
var graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), islandArea);
var islandCells = CaveGenerator.GraphEdgesToCells(graphEdges, islandArea, GridCellSize, out var cellGrid);
//make the island elliptical
for (int j = islandCells.Count - 1; j >= 0; j--)
{
var cell = islandCells[j];
double xDiff = (cell.Site.Coord.X - islandArea.Center.X) / (islandArea.Width * 0.5);
double yDiff = (cell.Site.Coord.Y - islandArea.Center.Y) / (islandArea.Height * 0.5);
//a conical stalactite-like shape at the bottom
if (yDiff < 0) { xDiff += xDiff * Math.Abs(yDiff); }
double normalizedDist = Math.Sqrt(xDiff * xDiff + yDiff * yDiff);
if (normalizedDist > 0.95 ||
cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.X)) ||
cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.Right)) ||
cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Y)) ||
cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Bottom)))
{
islandCells[j].CellType = CellType.Removed;
islandCells.RemoveAt(j);
}
}
var caveParams = CaveGenerationParams.GetRandom(this, abyss: true, rand: Rand.RandSync.ServerAndClient);
float caveScaleRelativeToIsland = 0.7f;
GenerateCave(
caveParams, Tunnels.First(),
new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2),
new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland)));
AbyssIslands.Add(new AbyssIsland(islandArea, islandCells));
createdCaves++;
}
}
private void GenerateSeaFloorPositions()
{
BottomPos = GenerationParams.SeaFloorDepth;
SeaFloorTopPos = BottomPos;
bottomPositions = new List<Point>
{
new Point(0, BottomPos)
};
int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.ServerAndClient);
for (int i = 0; i < mountainCount; i++)
{
bottomPositions.Add(
new Point(Size.X / (mountainCount + 1) * (i + 1),
BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.ServerAndClient)));
}
bottomPositions.Add(new Point(Size.X, BottomPos));
int minVertexInterval = 5000;
float currInverval = Size.X / 2;
while (currInverval > minVertexInterval)
{
for (int i = 0; i < bottomPositions.Count - 1; i++)
{
bottomPositions.Insert(i + 1,
new Point(
(bottomPositions[i].X + bottomPositions[i + 1].X) / 2,
(bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.ServerAndClient)));
i++;
}
currInverval /= 2;
}
SeaFloorTopPos = bottomPositions.Max(p => p.Y);
}
private void GenerateSeaFloor()
{
SeaFloor = new LevelWall(bottomPositions.Select(p => p.ToVector2()).ToList(), new Vector2(0.0f, -2000.0f), GenerationParams.WallColor, this);
ExtraWalls.Add(SeaFloor);
BottomBarrier = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(borders.X, 0)),
ConvertUnits.ToSimUnits(new Vector2(borders.Right, 0)));
BottomBarrier.SetTransform(ConvertUnits.ToSimUnits(new Vector2(0.0f, BottomPos)), 0.0f);
BottomBarrier.BodyType = BodyType.Static;
BottomBarrier.CollisionCategories = Physics.CollisionLevel;
bodies.Add(BottomBarrier);
}
private void GenerateCaves(Tunnel parentTunnel)
{
for (int i = 0; i < GenerationParams.CaveCount; i++)
{
var caveParams = CaveGenerationParams.GetRandom(this, abyss: false, rand: Rand.RandSync.ServerAndClient);
Point caveSize = new Point(
Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient),
Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient));
int padding = (int)(caveSize.X * 1.2f);
Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2);
int radius = Math.Max(caveSize.X, caveSize.Y) / 2;
var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.25f, asCloseAsPossible: true, allowedArea);
GenerateCave(caveParams, parentTunnel, cavePos, caveSize);
CalculateTunnelDistanceField(null);
}
}
private void GenerateCave(CaveGenerationParams caveParams, Tunnel parentTunnel, Point cavePos, Point caveSize)
{
Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize);
Point closestParentNode = parentTunnel.Nodes.First();
double closestDist = double.PositiveInfinity;
foreach (Point node in parentTunnel.Nodes)
{
if (caveArea.Contains(node)) { continue; }
double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y);
if (dist < closestDist)
{
closestParentNode = node;
closestDist = dist;
}
}
if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
{
caveStartPosVector = caveArea.Location.ToVector2();
}
Point caveStartPos = caveStartPosVector.ToPoint();
Point caveEndPos = cavePos - (caveStartPos - cavePos);
Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos);
Caves.Add(cave);
var caveSegments = MathUtils.GenerateJaggedLine(
caveStartPos.ToVector2(), caveEndPos.ToVector2(),
iterations: 3,
offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f,
bounds: caveArea);
if (!caveSegments.Any()) { return; }
List<Tunnel> caveBranches = new List<Tunnel>();
var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 150, parentTunnel);
Tunnels.Add(tunnel);
caveBranches.Add(tunnel);
int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient);
for (int j = 0; j < branches; j++)
{
Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient);
Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2();
Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2();
var branchSegments = MathUtils.GenerateJaggedLine(
branchStartPos, branchEndPos,
iterations: 3,
offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f,
bounds: caveArea);
if (!branchSegments.Any()) { continue; }
var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch);
Tunnels.Add(branch);
caveBranches.Add(branch);
}
foreach (Tunnel branch in caveBranches)
{
var node = branch.Nodes.Last();
PositionsOfInterest.Add(new InterestingPosition(node, node.Y < AbyssArea.Bottom ? PositionType.AbyssCave : PositionType.Cave, cave));
cave.Tunnels.Add(branch);
}
static List<Point> SegmentsToNodes(List<Vector2[]> segments)
{
List<Point> nodes = new List<Point>();
foreach (Vector2[] segment in segments)
{
nodes.Add(segment[0].ToPoint());
}
nodes.Add(segments.Last()[1].ToPoint());
return nodes;
}
}
private void GenerateRuin(Point ruinPos, bool mirror)
{
var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient);
LocationType locationType = StartLocation?.Type;
if (locationType == null)
{
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
if (ruinGenerationParams.AllowedLocationTypes.Any())
{
locationType = LocationType.Prefabs.Where(lt =>
ruinGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient);
}
}
var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror);
Ruins.Add(ruin);
var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4);
foreach (VoronoiCell cell in tooClose)
{
if (cell.CellType == CellType.Empty) { continue; }
if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; }
foreach (GraphEdge e in cell.Edges)
{
if (ruin.Area.Contains(e.Point1) || ruin.Area.Contains(e.Point2) ||
MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, ruin.Area, out _))
{
cell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Remove(cell);
}
}
cells.Remove(cell);
break;
}
}
}
ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center);
}
private void GenerateRuinWayPoints(Ruin ruin)
{
var tooClose = GetTooCloseCells(ruin.Area.Center.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 6);
List<WayPoint> wayPoints = new List<WayPoint>();
float outSideWaypointInterval = 500.0f;
WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
Rectangle waypointArea = ruin.Area;
waypointArea.Inflate(100, 100);
//generate waypoints around the ruin
for (int i = 0; i < 2; i++)
{
for (float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval)
{
var wayPoint = new WayPoint(new Vector2(x, waypointArea.Y + waypointArea.Height * i), SpawnType.Path, null)
{
Ruin = ruin
};
wayPoints.Add(wayPoint);
if (x == waypointArea.X + outSideWaypointInterval)
{
cornerWaypoint[i, 0] = wayPoint;
}
else
{
wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
}
}
cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1];
}
for (int i = 0; i < 2; i++)
{
WayPoint wayPoint = null;
for (float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval)
{
wayPoint = new WayPoint(new Vector2(waypointArea.X + waypointArea.Width * i, y), SpawnType.Path, null)
{
Ruin = ruin
};
wayPoints.Add(wayPoint);
if (y == waypointArea.Y)
{
wayPoint.ConnectTo(cornerWaypoint[0, i]);
}
else
{
wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
}
}
wayPoint.ConnectTo(cornerWaypoint[1, i]);
}
//remove waypoints that are inside walls
for (int i = wayPoints.Count - 1; i >= 0; i--)
{
WayPoint wp = wayPoints[i];
var overlappingCell = tooClose.Find(c => c.CellType != CellType.Removed && c.IsPointInside(wp.WorldPosition));
if (overlappingCell == null) { continue; }
if (wp.linkedTo.Count > 1)
{
WayPoint linked1 = wp.linkedTo[0] as WayPoint;
WayPoint linked2 = wp.linkedTo[1] as WayPoint;
linked1.ConnectTo(linked2);
}
wp.Remove();
wayPoints.RemoveAt(i);
}
Debug.Assert(wayPoints.Any(), "Couldn't generate waypoints around ruins.");
//connect ruin entrances to the outside waypoints
foreach (Gap g in Gap.GapList)
{
if (g.Submarine != ruin.Submarine || g.IsRoomToRoom || g.linkedTo.Count == 0) { continue; }
var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g);
if (gapWaypoint == null) { continue; }
//place another waypoint in front of the entrance
Vector2 entranceDir = Vector2.Zero;
if (g.IsHorizontal)
{
entranceDir = Vector2.UnitX * 2 * Math.Sign(g.WorldPosition.X - g.linkedTo[0].WorldPosition.X);
}
else
{
entranceDir = Vector2.UnitY * 2 * Math.Sign(g.WorldPosition.Y - g.linkedTo[0].WorldPosition.Y);
}
var entranceWayPoint = new WayPoint(g.WorldPosition + entranceDir * 64.0f, SpawnType.Path, null)
{
Ruin = ruin
};
entranceWayPoint.ConnectTo(gapWaypoint);
var closestWp = FindClosestWayPoint(entranceWayPoint.WorldPosition, wayPoints, (wp) =>
{
return Submarine.PickBody(
ConvertUnits.ToSimUnits(wp.WorldPosition),
ConvertUnits.ToSimUnits(entranceWayPoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
});
if (closestWp == null) { continue; }
ConnectWaypoints(entranceWayPoint, closestWp, outSideWaypointInterval);
}
//create a waypoint path from the ruin to the closest tunnel
WayPoint prevWp = FindClosestWayPoint(ruin.PathCells.First().Center, wayPoints, (wp) =>
{
return Submarine.PickBody(
ConvertUnits.ToSimUnits(wp.WorldPosition),
ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
});
if (prevWp != null)
{
for (int i = 0; i < ruin.PathCells.Count; i++)
{
var connectingEdge = i > 0 ? ruin.PathCells[i].Edges.Find(e => e.AdjacentCell(ruin.PathCells[i]) == ruin.PathCells[i - 1]) : null;
if (connectingEdge != null)
{
var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, edgeWayPoint, outSideWaypointInterval);
prevWp = edgeWayPoint;
}
var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval);
prevWp = newWaypoint;
}
var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, Tunnels.SelectMany(t => t.WayPoints));
ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval);
}
}
private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null)
{
var pointsAboveBottom = distanceField.FindAll(d => d.point.Y > GetBottomPosition(d.point.X).Y + minDistance);
if (pointsAboveBottom.Count == 0)
{
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?");
return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.ServerAndClient)].point;
}
var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point)));
if (!validPoints.Any())
{
DebugConsole.AddWarning("Failed to find a valid position far enough from the main path. Choosing the furthest possible position.\n" + Environment.StackTrace);
if (limits != null)
{
//try choosing something within the specified limits
validPoints = pointsAboveBottom.FindAll(d => limits.Value.Contains(d.point));
}
if (!validPoints.Any())
{
//couldn't find anything, let's just go with the furthest one
validPoints = pointsAboveBottom;
}
(Point position, double distance) furthestPoint = validPoints.First();
foreach (var point in validPoints)
{
if (point.distance > furthestPoint.distance)
{
furthestPoint = point;
}
}
return furthestPoint.position;
}
if (asCloseAsPossible)
{
if (!validPoints.Any()) { validPoints = distanceField; }
(Point position, double distance) closestPoint = validPoints.First();
foreach (var point in validPoints)
{
if (point.distance < closestPoint.distance)
{
closestPoint = point;
}
}
return closestPoint.position;
}
else
{
return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point;
}
}
private void CalculateTunnelDistanceField(List<Point> ruinPositions)
{
int density = 1000;
distanceField = new List<(Point point, double distance)>();
if (Mirrored)
{
for (int x = Size.X - 1; x >= 0; x -= density)
{
for (int y = 0; y < Size.Y; y += density)
{
addPoint(x, y);
}
}
}
else
{
for (int x = 0; x < Size.X; x += density)
{
for (int y = 0; y < Size.Y; y += density)
{
addPoint(x, y);
}
}
}
void addPoint(int x, int y)
{
Point point = new Point(x, y);
double shortestDistSqr = double.PositiveInfinity;
foreach (Tunnel tunnel in Tunnels)
{
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
}
}
if (ruinPositions != null)
{
int ruinSize = 10000;
foreach (Point ruinPos in ruinPositions)
{
double xDiff = Math.Abs(point.X - ruinPos.X);
double yDiff = Math.Abs(point.Y - ruinPos.Y);
if (xDiff < ruinSize && yDiff < ruinSize)
{
shortestDistSqr = 0.0f;
}
else
{
shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr);
}
}
}
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y));
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom));
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y));
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom));
distanceField.Add((point, Math.Sqrt(shortestDistSqr)));
}
}
private double GetDistToTunnel(Vector2 position, Tunnel tunnel)
{
Point point = position.ToPoint();
double shortestDistSqr = double.PositiveInfinity;
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
}
return Math.Sqrt(shortestDistSqr);
}
private DestructibleLevelWall CreateIceChunk(IEnumerable<GraphEdge> edges, Vector2 position, float? health = null)
{
List<Vector2> vertices = new List<Vector2>();
foreach (GraphEdge edge in edges)
{
if (!vertices.Any())
{
vertices.Add(edge.Point1);
}
else if (!vertices.Any(v => v.NearlyEquals(edge.Point1)))
{
vertices.Add(edge.Point1);
}
else if (!vertices.Any(v => v.NearlyEquals(edge.Point2)))
{
vertices.Add(edge.Point2);
}
}
if (vertices.Count < 3) { return null; }
return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health);
}
private DestructibleLevelWall CreateIceChunk(List<Vector2> vertices, Vector2 position, float? health = null)
{
DestructibleLevelWall newChunk = new DestructibleLevelWall(vertices, Color.White, this, health, true);
newChunk.Body.Position = ConvertUnits.ToSimUnits(position);
newChunk.Cells.ForEach(c => c.Translation = position);
newChunk.Body.BodyType = BodyType.Dynamic;
newChunk.Body.FixedRotation = true;
newChunk.Body.LinearDamping = 0.5f;
newChunk.Body.IgnoreGravity = true;
newChunk.Body.Mass *= 10.0f;
return newChunk;
}
private DestructibleLevelWall CreateIceSpire(List<GraphEdge> usedSpireEdges)
{
const float maxLength = 15000.0f;
float minEdgeLength = 100.0f;
var mainPathPos = PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient);
double closestDistSqr = double.PositiveInfinity;
GraphEdge closestEdge = null;
VoronoiCell closestCell = null;
foreach (VoronoiCell cell in cells)
{
if (cell.CellType != CellType.Solid) { continue; }
foreach (GraphEdge edge in cell.Edges)
{
if (!edge.IsSolid || usedSpireEdges.Contains(edge) || edge.NextToCave) { continue; }
//don't spawn spires near the start/end of the level
if (edge.Center.Y > Size.Y / 2 && (edge.Center.X < Size.X * 0.3f || edge.Center.X > Size.X * 0.7f)) { continue; }
if (Vector2.DistanceSquared(edge.Center, StartPosition) < maxLength * maxLength) { continue; }
if (Vector2.DistanceSquared(edge.Center, EndPosition) < maxLength * maxLength) { continue; }
//don't spawn on very long or very short edges
float edgeLengthSqr = Vector2.DistanceSquared(edge.Point1, edge.Point2);
if (edgeLengthSqr > 1000.0f * 1000.0f || edgeLengthSqr < minEdgeLength * minEdgeLength) { continue; }
//don't spawn on edges facing away from the main path
if (Vector2.Dot(Vector2.Normalize(mainPathPos.Position.ToVector2()) - edge.Center, edge.GetNormal(cell)) < 0.5f) { continue; }
double distSqr = MathUtils.DistanceSquared(edge.Center.X, edge.Center.Y, mainPathPos.Position.X, mainPathPos.Position.Y);
if (distSqr < closestDistSqr)
{
closestDistSqr = distSqr;
closestEdge = edge;
closestCell = cell;
}
}
}
if (closestEdge == null) { return null; }
usedSpireEdges.Add(closestEdge);
Vector2 edgeNormal = closestEdge.GetNormal(closestCell);
float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength);
spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f);
Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient);
List<Vector2> vertices = new List<Vector2>()
{
closestEdge.Point1,
extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient),
closestEdge.Point2,
};
Vector2 center = Vector2.Zero;
vertices.ForEach(v => center += v);
center /= vertices.Count;
DestructibleLevelWall spire = new DestructibleLevelWall(vertices.Select(v => v - center).ToList(), Color.White, this, health: 100.0f, giftWrap: true);
#if CLIENT
//make the edge at the bottom of the spire non-solid
foreach (GraphEdge edge in spire.Cells[0].Edges)
{
if ((edge.Point1.NearlyEquals(closestEdge.Point1 - center) && edge.Point2.NearlyEquals(closestEdge.Point2 - center)) ||
(edge.Point1.NearlyEquals(closestEdge.Point2 - center) && edge.Point2.NearlyEquals(closestEdge.Point1 - center)))
{
edge.IsSolid = false;
break;
}
}
spire.GenerateVertices();
#endif
spire.Body.Position = ConvertUnits.ToSimUnits(center);
spire.Body.BodyType = BodyType.Static;
spire.Body.FixedRotation = true;
spire.Body.IgnoreGravity = true;
spire.Body.Mass *= 10.0f;
spire.Cells.ForEach(c => c.Translation = center);
spire.WallDamageOnTouch = 50.0f;
return spire;
}
// TODO: Improve this temporary level editor debug solution (or remove it)
private static int nextPathPointId;
public List<PathPoint> PathPoints { get; } = new List<PathPoint>();
public struct PathPoint
{
public string Id { get; }
public Vector2 Position { get; }
public bool ShouldContainResources { get; set; }
public float NextClusterProbability
{
get
{
return ClusterLocations.Count switch
{
1 => 5.0f,
2 => 2.5f,
3 => 1.0f,
_ => 0.0f,
};
}
}
public List<Identifier> ResourceTags { get; }
public List<Identifier> ResourceIds { get; }
public List<ClusterLocation> ClusterLocations { get; }
public TunnelType TunnelType { get; }
private PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType, List<Identifier> resourceTags, List<Identifier> resourceIds, List<ClusterLocation> clusterLocations)
{
Id = id;
Position = position;
ShouldContainResources = shouldContainResources;
ResourceTags = resourceTags;
ResourceIds = resourceIds;
ClusterLocations = clusterLocations;
TunnelType = tunnelType;
}
public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType)
: this(id, position, shouldContainResources, tunnelType, new List<Identifier>(), new List<Identifier>(), new List<ClusterLocation>())
{
}
public PathPoint WithResources(bool containsResources)
{
return new PathPoint(Id, Position, containsResources, TunnelType, ResourceTags, ResourceIds, ClusterLocations);
}
}
public List<ClusterLocation> AbyssResources { get; } = new List<ClusterLocation>();
public struct ClusterLocation
{
public VoronoiCell Cell { get; }
public GraphEdge Edge { get; }
public Vector2 EdgeCenter { get; }
/// <summary>
/// Can be null unless initialized in constructor
/// </summary>
public List<Item> Resources { get; private set; }
/// <param name="initializeResourceList">List is initialized only when specified, otherwise will be null</param>
public ClusterLocation(VoronoiCell cell, GraphEdge edge, bool initializeResourceList = false)
{
Cell = cell;
Edge = edge;
EdgeCenter = edge.Center;
Resources = initializeResourceList ? new List<Item>() : null;
}
public bool Equals(ClusterLocation anotherLocation) =>
Cell == anotherLocation.Cell && Edge == anotherLocation.Edge;
public bool Equals(VoronoiCell cell, GraphEdge edge) =>
Cell == cell && Edge == edge;
public void InitializeResources()
{
Resources = new List<Item>();
}
}
// TODO: Take into account items which aren't ores or plants
// Such as the exploding crystals in The Great Sea
private void GenerateItems()
{
var levelResources = new List<(ItemPrefab itemPrefab, ItemPrefab.CommonnessInfo commonnessInfo)>();
var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>();
Vector2 commonnessRange = new Vector2(float.MaxValue, float.MinValue), caveCommonnessRange = new Vector2(float.MaxValue, float.MinValue);
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier))
{
if (itemPrefab.GetCommonnessInfo(this) is { CanAppear: true } commonnessInfo)
{
if (commonnessInfo.Commonness > 0.0)
{
if (commonnessInfo.Commonness < commonnessRange.X) { commonnessRange.X = commonnessInfo.Commonness; }
if (commonnessInfo.Commonness > commonnessRange.Y) { commonnessRange.Y = commonnessInfo.Commonness; }
}
if (commonnessInfo.CaveCommonness > 0.0)
{
if (commonnessInfo.CaveCommonness < caveCommonnessRange.X) { caveCommonnessRange.X = commonnessInfo.CaveCommonness; }
if (commonnessInfo.CaveCommonness > caveCommonnessRange.Y) { caveCommonnessRange.Y = commonnessInfo.CaveCommonness; }
}
levelResources.Add((itemPrefab, commonnessInfo));
}
else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) ||
itemPrefab.LevelQuantity.TryGetValue(LevelData.Biome.Identifier, out fixedQuantityResourceInfo) ||
itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo))
{
fixedResources.Add((itemPrefab, fixedQuantityResourceInfo));
}
}
DebugConsole.Log("Generating level resources...");
var allValidLocations = GetAllValidClusterLocations();
var maxResourceOverlap = 0.4f;
foreach (var (itemPrefab, resourceInfo) in fixedResources)
{
for (int i = 0; i < resourceInfo.ClusterQuantity; i++)
{
var location = allValidLocations.GetRandom(l =>
{
if (l.Cell == null || l.Edge == null) { return false; }
if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; }
if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
}, randSync: Rand.RandSync.ServerAndClient);
if (location.Cell == null || location.Edge == null) { break; }
PlaceResources(itemPrefab, resourceInfo.ClusterSize, location, out _);
var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
allValidLocations.RemoveAt(locationIndex);
}
}
// Abyss Resources
AbyssResources.Clear();
var abyssResourcePrefabs = levelResources.Where(r => r.commonnessInfo.AbyssCommonness > 0.0f);
if (abyssResourcePrefabs.Any())
{
int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, MathUtils.InverseLerp(LevelData.Biome.MinDifficulty, LevelData.Biome.AdjustedMaxDifficulty, Difficulty));
for (int i = 0; i < abyssClusterCount; i++)
{
var selectedPrefab = ToolBox.SelectWeightedRandom(
abyssResourcePrefabs.Select(r => r.itemPrefab).ToList(),
abyssResourcePrefabs.Select(r => r.commonnessInfo.AbyssCommonness).ToList(),
Rand.RandSync.ServerAndClient);
var location = allValidLocations.GetRandom(l =>
{
if (l.Cell == null || l.Edge == null) { return false; }
if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; }
l.InitializeResources();
return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _);
}, randSync: Rand.RandSync.ServerAndClient);
if (location.Cell == null || location.Edge == null) { break; }
int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient);
PlaceResources(selectedPrefab, clusterSize, location, out var placedResources, maxResourceOverlap: 0);
var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true);
abyssClusterLocation.Resources.AddRange(placedResources);
AbyssResources.Add(abyssClusterLocation);
var locationIndex = allValidLocations.FindIndex(l => l.Equals(location));
allValidLocations.RemoveAt(locationIndex);
}
}
PathPoints.Clear();
nextPathPointId = 0;
foreach (Tunnel tunnel in Tunnels)
{
var tunnelLength = 0.0f;
for (int i = 1; i < tunnel.Nodes.Count; i++)
{
tunnelLength += Vector2.Distance(tunnel.Nodes[i - 1].ToVector2(), tunnel.Nodes[i].ToVector2());
}
var nextNodeIndex = 1;
var positionOnPath = tunnel.Nodes.First().ToVector2();
var lastNodePos = tunnel.Nodes.Last().ToVector2();
var reachedLastNode = false;
var intervalRange = tunnel.Type != TunnelType.Cave ? GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange;
do
{
var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient);
reachedLastNode = !CalculatePositionOnPath();
var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++;
var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ?
GenerationParams.CaveResourceSpawnChance : GenerationParams.ResourceSpawnChance;
var containsResources = true;
if (spawnChance < 1.0f)
{
var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient);
containsResources = spawnPointRoll <= spawnChance;
}
var tunnelType = tunnel.Type;
if (tunnel.ParentTunnel != null && tunnel.ParentTunnel.Type == TunnelType.Cave) { tunnelType = TunnelType.Cave; }
PathPoints.Add(new PathPoint(id, positionOnPath, containsResources, tunnel.Type));
bool CalculatePositionOnPath(float checkedDist = 0.0f)
{
if (nextNodeIndex >= tunnel.Nodes.Count) { return false; }
var distToNextNode = Vector2.Distance(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2());
var lerpAmount = (distance - checkedDist) / distToNextNode;
if (lerpAmount <= 1.0f)
{
positionOnPath = Vector2.Lerp(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2(), lerpAmount);
return true;
}
else
{
positionOnPath = tunnel.Nodes[nextNodeIndex++].ToVector2();
return CalculatePositionOnPath(checkedDist + distToNextNode);
}
}
} while (!reachedLastNode && Vector2.DistanceSquared(positionOnPath, lastNodePos) > (intervalRange.Y * intervalRange.Y));
}
int itemCount = 0;
Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() };
var disabledPathPoints = new List<string>();
// Create first cluster for each spawn point
foreach (var pathPoint in PathPoints)
{
if (itemCount >= GenerationParams.ItemCount) { break; }
if (!pathPoint.ShouldContainResources) { continue; }
GenerateFirstCluster(pathPoint);
if (pathPoint.ClusterLocations.Count > 0) { continue; }
disabledPathPoints.Add(pathPoint.Id);
}
// Don't try to spawn more resource clusters for points for which the initial cluster could not be spawned
foreach (string pathPointId in disabledPathPoints)
{
if (PathPoints.FirstOrNull(p => p.Id == pathPointId) is PathPoint pathPoint)
{
PathPoints.RemoveAll(p => p.Id == pathPointId);
PathPoints.Add(pathPoint.WithResources(false));
}
}
var excludedPathPointIds = new List<string>();
while (itemCount < GenerationParams.ItemCount)
{
var availablePathPoints = PathPoints.Where(p =>
p.ShouldContainResources && p.NextClusterProbability > 0 &&
!excludedPathPointIds.Contains(p.Id)).ToList();
if (availablePathPoints.None()) { break; }
var pathPoint = ToolBox.SelectWeightedRandom(
availablePathPoints,
availablePathPoints.Select(p => p.NextClusterProbability).ToList(),
Rand.RandSync.ServerAndClient);
GenerateAdditionalCluster(pathPoint);
}
#if DEBUG
int spawnPointsContainingResources = PathPoints.Where(p => p.ClusterLocations.Any()).Count();
string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count);
DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" +
$" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" +
$" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk");
if (AbyssResources.Count > 0)
{
DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" +
$" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk");
}
#endif
DebugConsole.Log("Level resources generated");
bool GenerateFirstCluster(PathPoint pathPoint)
{
var intervalRange = pathPoint.TunnelType != TunnelType.Cave ?
GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange;
allValidLocations.Sort((x, y) =>
Vector2.DistanceSquared(pathPoint.Position, x.EdgeCenter)
.CompareTo(Vector2.DistanceSquared(pathPoint.Position, y.EdgeCenter)));
var selectedLocationIndex = -1;
var generatedCluster = false;
for (int i = 0; i < allValidLocations.Count; i++)
{
var validLocation = allValidLocations[i];
if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) { continue; }
if (validLocation.EdgeCenter.Y < AbyssArea.Bottom) { continue; }
var distanceSquaredToEdge = Vector2.DistanceSquared(pathPoint.Position, validLocation.EdgeCenter);
// Edge isn't too far from the path point
if (distanceSquaredToEdge > 3.0f * (intervalRange.Y * intervalRange.Y)) { continue; }
// Edge is closer to the path point than the cell center
if (distanceSquaredToEdge > Vector2.DistanceSquared(pathPoint.Position, validLocation.Cell.Center)) { continue; }
var validComparedToOtherPathPoints = true;
// Make sure this path point is closest to 'validLocation'
foreach (var anotherPathPoint in PathPoints)
{
if (anotherPathPoint.Id == pathPoint.Id) { continue; }
if (Vector2.DistanceSquared(anotherPathPoint.Position, validLocation.EdgeCenter) < distanceSquaredToEdge)
{
validComparedToOtherPathPoints = false;
break;
}
}
foreach (var anotherPathPoint in PathPoints.Where(p => p.Id != pathPoint.Id && p.ClusterLocations.Any()))
{
if (!validComparedToOtherPathPoints) { break; }
foreach (var c in pathPoint.ClusterLocations)
{
if (IsInvalidComparedToExistingLocation())
{
validComparedToOtherPathPoints = false;
break;
}
bool IsInvalidComparedToExistingLocation()
{
if (c.Equals(validLocation)) { return true; }
// If there is a previously spawned cluster too near
if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) { return true; }
// If there is a line from a previous path point to one of its existing cluster locations
// which intersects with the line from this path point to the new possible cluster location
if (MathUtils.LinesIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; }
return false;
}
}
}
if (!validComparedToOtherPathPoints) { continue; }
generatedCluster = CreateResourceCluster(pathPoint, validLocation);
selectedLocationIndex = i;
break;
}
if (selectedLocationIndex >= 0)
{
allValidLocations.RemoveAt(selectedLocationIndex);
}
return generatedCluster;
static bool IsNextToTunnelType(GraphEdge e, TunnelType t) =>
(e.NextToMainPath && t == TunnelType.MainPath) ||
(e.NextToSidePath && t == TunnelType.SidePath) ||
(e.NextToCave && t == TunnelType.Cave);
}
bool GenerateAdditionalCluster(PathPoint pathPoint)
{
var validLocations = new List<ClusterLocation>();
// First check only the edges of the same cell
// which are connected to one of the existing edges with clusters
foreach (var clusterLocation in pathPoint.ClusterLocations)
{
foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
{
if (HaveConnectingEdgePoints(anotherEdge, clusterLocation.Edge))
{
AddIfValid(clusterLocation.Cell, anotherEdge);
}
}
}
// Only check edges of adjacent cells if no valid edges were found
// on any of the cells with existing clusters
if (validLocations.None())
{
foreach (var clusterLocation in pathPoint.ClusterLocations)
{
foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge))
{
var adjacentCell = anotherEdge.AdjacentCell(clusterLocation.Cell);
if (adjacentCell == null) { continue; }
foreach (var adjacentCellEdge in adjacentCell.Edges.Where(e => e != anotherEdge))
{
if (HaveConnectingEdgePoints(adjacentCellEdge, clusterLocation.Edge))
{
AddIfValid(adjacentCell, adjacentCellEdge);
}
}
}
}
}
if (validLocations.Any())
{
var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient);
if (CreateResourceCluster(pathPoint, location))
{
var i = allValidLocations.FindIndex(l => l.Equals(location));
if (i >= 0)
{
allValidLocations.RemoveAt(i);
}
return true;
}
else
{
excludedPathPointIds.Add(pathPoint.Id);
return false;
}
}
else
{
excludedPathPointIds.Add(pathPoint.Id);
return false;
}
static bool HaveConnectingEdgePoints(GraphEdge e1, GraphEdge e2) =>
e1.Point1.NearlyEquals(e2.Point1) || e1.Point1.NearlyEquals(e2.Point2) ||
e1.Point2.NearlyEquals(e2.Point1) || e1.Point2.NearlyEquals(e2.Point2);
void AddIfValid(VoronoiCell c, GraphEdge e)
{
if (IsAlreadyInList(e)) { return; }
if (allValidLocations.None(l => l.Equals(c, e))) { return; }
if (pathPoint.ClusterLocations.Any(cl => cl.Edge == e)) { return; }
validLocations.Add(new ClusterLocation(c, e));
}
bool IsAlreadyInList(GraphEdge edge) =>
validLocations.Any(l => l.Edge == edge);
}
bool CreateResourceCluster(PathPoint pathPoint, ClusterLocation location)
{
if (location.Cell == null || location.Edge == null) { return false; }
ItemPrefab selectedPrefab;
if (pathPoint.ClusterLocations.Count == 0)
{
selectedPrefab = ToolBox.SelectWeightedRandom(
levelResources.Select(it => it.itemPrefab).ToList(),
levelResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
Rand.RandSync.ServerAndClient);
selectedPrefab.Tags.ForEach(t =>
{
if (exclusiveResourceTags.Contains(t))
{
pathPoint.ResourceTags.Add(t);
}
});
}
else
{
var filteredResources = pathPoint.ResourceTags.None() ? levelResources :
levelResources.Where(it => it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t)));
selectedPrefab = ToolBox.SelectWeightedRandom(
filteredResources.Select(it => it.itemPrefab).ToList(),
filteredResources.Select(it => it.commonnessInfo.GetCommonness(pathPoint.TunnelType)).ToList(),
Rand.RandSync.ServerAndClient);
}
if (selectedPrefab == null) { return false; }
// Create resources for the cluster
float commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonnessInfo.GetCommonness(pathPoint.TunnelType);
float lerpAmount = pathPoint.TunnelType != TunnelType.Cave ?
MathUtils.InverseLerp(commonnessRange.X, commonnessRange.Y, commonness) :
MathUtils.InverseLerp(caveCommonnessRange.X, caveCommonnessRange.Y, commonness);
var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount);
var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength);
maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge);
if (itemCount + maxClusterSize > GenerationParams.ItemCount)
{
maxClusterSize += GenerationParams.ItemCount - (itemCount + maxClusterSize);
}
if (maxClusterSize < 1) { return false; }
var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize);
var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient);
if (resourcesInCluster < 1) { return false; }
PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLength: edgeLength);
itemCount += resourcesInCluster;
location.InitializeResources();
location.Resources.AddRange(placedResources);
pathPoint.ClusterLocations.Add(location);
pathPoint.ResourceIds.Add(selectedPrefab.Identifier);
return true;
}
int GetMaxResourcesOnEdge(ItemPrefab resourcePrefab, ClusterLocation location, out float edgeLength)
{
edgeLength = 0.0f;
if (location.Cell == null || location.Edge == null) { return 0; }
edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
if (resourcePrefab == null) { return 0; }
return (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X));
}
}
/// <param name="rotation">Used by clients to set the rotation for the resources</param>
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable<Cave> targetCaves = null)
{
var allValidLocations = GetAllValidClusterLocations();
var placedResources = new List<Item>();
rotation = 0.0f;
if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?!
// Make sure not to pick a spot that already has other level resources
for (int i = allValidLocations.Count - 1; i >= 0; i--)
{
if (HasResources(allValidLocations[i]))
{
allValidLocations.RemoveAt(i);
}
bool HasResources(ClusterLocation clusterLocation)
{
foreach (var p in PathPoints)
{
foreach (var c in p.ClusterLocations)
{
if (!c.Equals(clusterLocation)) { continue; }
foreach (var r in c.Resources)
{
if (r == null) { continue; }
if (r.Removed) { continue; }
if (!(r.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)) { return true; }
}
}
}
return false;
}
}
if (PositionsOfInterest.None(p => p.PositionType == positionType))
{
DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources.");
foreach (var validType in MineralMission.ValidPositionTypes)
{
if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType))
{
DebugConsole.AddWarning($"Placing in \"{validType}\" instead.");
positionType = validType;
break;
}
}
}
try
{
RemoveInvalidLocations(positionType switch
{
PositionType.MainPath => IsOnMainPath,
PositionType.SidePath => IsOnSidePath,
PositionType.Cave => IsInCave,
PositionType.AbyssCave => IsInAbyssCave,
_ => throw new NotImplementedException(),
});
}
catch (NotImplementedException)
{
DebugConsole.ThrowError($"Unexpected PositionType (\"{positionType}\") for mineral mission resources: mineral spawning might not work as expected.");
}
if (targetCaves != null && targetCaves.Any())
{
// If resources are placed inside a cave, make sure all of them are placed inside the same one
allValidLocations.RemoveAll(l => targetCaves.None(c => c.Area.Contains(l.EdgeCenter)));
}
var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient);
Vector2 poiPos = poi.Position.ToVector2();
allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter)
.CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter)));
float maxResourceOverlap = 0.4f;
var selectedLocation = allValidLocations.FirstOrDefault(l =>
Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength &&
!l.Edge.OutsideLevel &&
((l.Edge.Cell1?.IsDestructible ?? false) || (l.Edge.Cell2?.IsDestructible ?? false)) &&
requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X)));
if (selectedLocation.Edge == null)
{
//couldn't find a long enough edge, find the largest one
float longestEdge = 0.0f;
foreach (var validLocation in allValidLocations)
{
if (Vector2.Distance(validLocation.Edge.Point1, validLocation.Edge.Point2) is float edgeLength && edgeLength > longestEdge)
{
selectedLocation = validLocation;
longestEdge = edgeLength;
}
}
}
if (selectedLocation.Edge == null)
{
throw new Exception("Failed to find a suitable level wall edge to place level resources on.");
}
PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
return placedResources;
static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath;
static bool IsOnSidePath(ClusterLocation location) => location.Edge.NextToSidePath;
static bool IsInCave(ClusterLocation location) => location.Edge.NextToCave;
bool IsInAbyssCave(ClusterLocation location) => location.EdgeCenter.Y < AbyssStart;
void RemoveInvalidLocations(Predicate<ClusterLocation> match)
{
allValidLocations.RemoveAll(l => !match(l));
}
}
private List<ClusterLocation> GetAllValidClusterLocations()
{
var subBorders = new List<Rectangle>();
Wrecks.ForEach(w => AddBordersToList(w));
AddBordersToList(BeaconStation);
var locations = new List<ClusterLocation>();
foreach (var c in GetAllCells())
{
if (c.CellType != CellType.Solid) { continue; }
foreach (var e in c.Edges)
{
if (IsValidEdge(e))
{
locations.Add(new ClusterLocation(c, e));
}
}
}
return locations;
void AddBordersToList(Submarine s)
{
if (s == null) { return; }
var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2());
subBorders.Add(rect);
}
bool IsValidEdge(GraphEdge e)
{
if (!e.IsSolid) { return false; }
if (e.OutsideLevel) { return false; }
var eCenter = e.Center;
if (IsBlockedByWreckOrBeacon()) { return false; }
if (IsBlockedByWall()) { return false; }
return true;
bool IsBlockedByWreckOrBeacon()
{
foreach (var r in subBorders)
{
if (Submarine.RectContains(r, e.Point1)) { return true; }
if (Submarine.RectContains(r, e.Point2)) { return true; }
if (Submarine.RectContains(r, eCenter)) { return true; }
}
return false;
}
bool IsBlockedByWall()
{
foreach (var w in ExtraWalls)
{
foreach (var c in w.Cells)
{
if (c.IsPointInside(eCenter)) { return true; }
if (c.IsPointInside(eCenter - 100 * e.GetNormal(c))) { return true; }
if (c.Edges.Any(extraWallEdge => extraWallEdge == e)) { return true; }
}
}
return false;
}
}
}
private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List<Item> placedResources,
float? edgeLength = null, float maxResourceOverlap = 0.4f)
{
edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2);
Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value;
if (!MathUtils.IsValid(edgeDir))
{
edgeDir = Vector2.Zero;
}
var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X));
minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap);
var lerpAmounts = new float[resourceCount];
lerpAmounts[0] = 0.0f;
var lerpAmount = 0.0f;
for (int i = 1; i < resourceCount; i++)
{
var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient);
lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value;
lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f);
}
var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient);
placedResources = new List<Item>();
for (int i = 0; i < resourceCount; i++)
{
Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]);
var item = new Item(resourcePrefab, selectedPos, submarine: null);
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
item.Move(edgeNormal * moveAmount, ignoreContacts: true);
if (item.GetComponent<Holdable>() is Holdable h)
{
h.AttachToWall();
#if CLIENT
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
#endif
}
else if (item.body != null)
{
item.body.SetTransformIgnoreContacts(item.body.SimPosition, MathUtils.VectorToAngle(edgeNormal) - MathHelper.PiOver2);
}
placedResources.Add(item);
}
}
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f, Func<InterestingPosition, bool> filter = null)
{
if (!PositionsOfInterest.Any())
{
return new Vector2(Size.X / 2, Size.Y / 2);
}
Vector2 position = Vector2.Zero;
int tries = 0;
do
{
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
if (!IsPositionInsideWall(startPos + offset))
{
startPos += offset;
}
Vector2 endPos = startPos - Vector2.UnitY * Size.Y;
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(startPos),
ConvertUnits.ToSimUnits(endPos),
ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)),
Physics.CollisionLevel | Physics.CollisionWall) != null)
{
position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall;
break;
}
tries++;
if (tries == 10)
{
position = startPos;
}
} while (tries < 10);
return position;
}
public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Vector2 awayPoint, float minDistFromPoint, Func<InterestingPosition, bool> filter = null)
{
bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, awayPoint, minDistFromPoint, filter);
position = pos.ToVector2();
return success;
}
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func<InterestingPosition, bool> filter = null, bool suppressWarning = false)
{
bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning);
position = pos.ToVector2();
return success;
}
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func<InterestingPosition, bool> filter = null, bool suppressWarning = false)
{
if (!PositionsOfInterest.Any())
{
position = new Point(Size.X / 2, Size.Y / 2);
return false;
}
List<InterestingPosition> suitablePositions = PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
if (filter != null)
{
suitablePositions.RemoveAll(p => !filter(p));
}
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) ||
positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave))
{
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
}
if (!suitablePositions.Any())
{
if (!suppressWarning)
{
string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
}
position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
return false;
}
List<InterestingPosition> farEnoughPositions = new List<InterestingPosition>(suitablePositions);
if (minDistFromSubs > 0.0f)
{
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player) { continue; }
farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs);
}
}
if (minDistFromPoint > 0.0f)
{
farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), awayPoint) < minDistFromPoint * minDistFromPoint);
}
if (!farEnoughPositions.Any())
{
string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
float maxDist = 0.0f;
position = suitablePositions.First().Position;
foreach (InterestingPosition pos in suitablePositions)
{
float dist = Submarine.Loaded.Sum(s =>
Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f);
if (dist > maxDist)
{
position = pos.Position;
maxDist = dist;
}
}
return false;
}
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position;
return true;
}
public bool IsPositionInsideWall(Vector2 worldPosition)
{
var closestCell = GetClosestCell(worldPosition);
return closestCell != null && closestCell.IsPointInside(worldPosition);
}
public void Update(float deltaTime, Camera cam)
{
LevelObjectManager.Update(deltaTime);
foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); }
for (int i = UnsyncedExtraWalls.Count - 1; i >= 0; i--)
{
UnsyncedExtraWalls[i].Update(deltaTime);
}
#if SERVER
if (GameMain.NetworkMember is { IsServer: true })
{
foreach (LevelWall wall in ExtraWalls)
{
if (wall is DestructibleLevelWall { NetworkUpdatePending: true } destructibleWall)
{
GameMain.NetworkMember.CreateEntityEvent(this, new SingleLevelWallEventData(destructibleWall));
destructibleWall.NetworkUpdatePending = false;
}
}
networkUpdateTimer += deltaTime;
if (networkUpdateTimer > NetworkUpdateInterval)
{
if (ExtraWalls.Any(w => w.Body.BodyType != BodyType.Static))
{
GameMain.NetworkMember.CreateEntityEvent(this, new GlobalLevelWallEventData());
}
networkUpdateTimer = 0.0f;
}
}
#endif
#if CLIENT
backgroundCreatureManager.Update(deltaTime, cam);
WaterRenderer.Instance?.ScrollWater(Vector2.UnitY, (float)deltaTime);
renderer.Update(deltaTime, cam);
#endif
}
public Vector2 GetBottomPosition(float xPosition)
{
float interval = Size.X / (bottomPositions.Count - 1);
int index = (int)Math.Floor(xPosition / interval);
if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
float t = (xPosition - bottomPositions[index].X) / interval;
t = MathHelper.Clamp(t, 0.0f, 1.0f);
float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
return new Vector2(xPosition, yPos);
}
public List<VoronoiCell> GetAllCells()
{
List<VoronoiCell> cells = new List<VoronoiCell>();
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cells.AddRange(cellGrid[x, y]);
}
}
return cells;
}
private readonly List<VoronoiCell> tempCells = new List<VoronoiCell>();
public List<VoronoiCell> GetCells(Vector2 worldPos, int searchDepth = 2)
{
tempCells.Clear();
int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize);
int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize);
int startX = Math.Max(gridPosX - searchDepth, 0);
int endX = Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1);
int startY = Math.Max(gridPosY - searchDepth, 0);
int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1);
for (int y = startY; y <= endY; y++)
{
for (int x = startX; x <= endX; x++)
{
tempCells.AddRange(cellGrid[x, y]);
}
}
foreach (LevelWall wall in ExtraWalls)
{
if (wall == SeaFloor)
{
if (SeaFloorTopPos < worldPos.Y - searchDepth * GridCellSize) { continue; }
}
else
{
if (wall is DestructibleLevelWall destructibleWall && destructibleWall.Destroyed) { continue; }
bool closeEnough = false;
foreach (VoronoiCell cell in wall.Cells)
{
if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2))
{
closeEnough = true;
break;
}
}
if (!closeEnough) { continue; }
}
foreach (VoronoiCell cell in wall.Cells)
{
tempCells.Add(cell);
}
}
foreach (var abyssIsland in AbyssIslands)
{
if (abyssIsland.Area.X > worldPos.X + searchDepth * GridCellSize) { continue; }
if (abyssIsland.Area.Right < worldPos.X - searchDepth * GridCellSize) { continue; }
if (abyssIsland.Area.Y > worldPos.Y + searchDepth * GridCellSize) { continue; }
if (abyssIsland.Area.Bottom < worldPos.Y - searchDepth * GridCellSize) { continue; }
tempCells.AddRange(abyssIsland.Cells);
}
return tempCells;
}
public VoronoiCell GetClosestCell(Vector2 worldPos)
{
double closestDist = double.MaxValue;
VoronoiCell closestCell = null;
int searchDepth = 2;
while (searchDepth < 5)
{
foreach (var cell in GetCells(worldPos, searchDepth))
{
double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, worldPos.X, worldPos.Y);
if (dist < closestDist)
{
closestDist = dist;
closestCell = cell;
}
}
if (closestCell != null) { break; }
searchDepth++;
}
return closestCell;
}
private List<VoronoiCell> CreatePathToClosestTunnel(Point pos)
{
VoronoiCell closestPathCell = null;
double closestDist = 0.0f;
foreach (Tunnel tunnel in Tunnels)
{
if (tunnel.Type == TunnelType.Cave) { continue; }
foreach (VoronoiCell cell in tunnel.Cells)
{
double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, pos.X, pos.Y);
if (closestPathCell == null || dist < closestDist)
{
closestPathCell = cell;
closestDist = dist;
}
}
}
//cast a ray from the closest path cell towards the position and remove the cells it hits
List<VoronoiCell> validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed);
List<VoronoiCell> pathCells = new List<VoronoiCell>() { closestPathCell };
foreach (VoronoiCell cell in validCells)
{
foreach (GraphEdge e in cell.Edges)
{
if (!MathUtils.LinesIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; }
cell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Remove(cell);
}
}
pathCells.Add(cell);
cells.Remove(cell);
//go through the edges of this cell and find the ones that are next to a removed cell
foreach (var otherEdge in cell.Edges)
{
var otherAdjacent = otherEdge.AdjacentCell(cell);
if (otherAdjacent == null || otherAdjacent.CellType == CellType.Solid) { continue; }
//if the edge is very short, remove adjacent cells to prevent making the passage too narrow
if (Vector2.DistanceSquared(otherEdge.Point1, otherEdge.Point2) < 500.0f * 500.0f)
{
foreach (GraphEdge e2 in cell.Edges)
{
if (e2 == otherEdge || e2 == otherEdge) { continue; }
if (!MathUtils.NearlyEqual(otherEdge.Point1, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point2))
{
continue;
}
var adjacentCell = e2.AdjacentCell(cell);
if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; }
adjacentCell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Remove(adjacentCell);
}
}
cells.Remove(adjacentCell);
}
}
}
break;
}
}
pathCells.Sort((c1, c2) => { return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); });
return pathCells;
}
public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist);
public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist);
public bool IsCloseToStart(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(startPosition, startExitPosition, position) < minDist * minDist;
}
public bool IsCloseToEnd(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist;
}
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type)
{
var tempSW = new Stopwatch();
// Min distance between a sub and the start/end/other sub.
const float minDistance = Sonar.DefaultSonarRange;
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < EndExitPosition.X &&
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value)
{
Type = type
};
//place downwards by default
var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom;
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000);
var positions = new List<Vector2>();
var rects = new List<Rectangle>();
int maxAttempts = 50;
int attemptsLeft = maxAttempts;
bool success = false;
Vector2 spawnPoint = Vector2.Zero;
var allCells = Loaded.GetAllCells();
while (attemptsLeft > 0)
{
if (attemptsLeft < maxAttempts)
{
Debug.WriteLine($"Failed to position the sub {subName}. Trying again.");
}
attemptsLeft--;
if (TryGetSpawnPoint(out spawnPoint))
{
success = TryPositionSub(subBorders, subName, placement, ref spawnPoint);
if (success)
{
break;
}
else
{
positions.Clear();
}
}
else
{
DebugConsole.NewMessage($"Failed to find any spawn point for the sub: {subName} (No valid waypoints left).", Color.Red);
break;
}
}
tempSW.Stop();
if (success)
{
Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
tempSW.Restart();
Submarine sub = new Submarine(info);
if (type == SubmarineType.Wreck)
{
sub.MakeWreck();
Wrecks.Add(sub);
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub));
foreach (Hull hull in sub.GetHulls(false))
{
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance)
{
hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient);
}
}
// Only spawn thalamus when the wreck has some thalamus items defined.
if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus")))
{
if (!sub.CreateWreckAI())
{
DebugConsole.NewMessage($"Failed to create wreck AI inside {subName}.", Color.Red);
sub.DisableWreckAI();
}
}
else
{
sub.DisableWreckAI();
}
}
else if (type == SubmarineType.BeaconStation)
{
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
sub.ShowSonarMarker = false;
sub.DockedTo.ForEach(s => s.ShowSonarMarker = false);
sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
sub.TeamID = CharacterTeamType.None;
}
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
sub.SetPosition(spawnPoint);
wreckPositions.Add(sub, positions);
blockedRects.Add(sub, rects);
return sub;
}
else
{
DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).", Color.Red);
return null;
}
bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint)
{
positions.Add(spawnPoint);
bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
positions.Add(spawnPoint);
bool leftSideBlocked = IsSideBlocked(subBorders, false);
bool rightSideBlocked = IsSideBlocked(subBorders, true);
int step = 5;
if (rightSideBlocked && !leftSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
}
else if (leftSideBlocked && !rightSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
}
else if (!bottomFound)
{
if (!leftSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
}
else if (!rightSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
}
else
{
Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
return false;
}
}
positions.Add(spawnPoint);
bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50));
if (isBlocked)
{
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size));
Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls.");
}
else if (!bottomFound)
{
Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
}
else
{
var sp = spawnPoint;
if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
{
Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s).");
return false;
}
}
return !isBlocked && bottomFound;
bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount)
{
float maxMovement = 5000;
float totalAmount = 0;
bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
while (!IsSideBlocked(subBorders, amount > 0))
{
foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
totalAmount += amount;
spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y);
if (Math.Abs(totalAmount) > maxMovement)
{
Debug.WriteLine($"Moving the sub {subName} failed.");
break;
}
}
return foundBottom;
}
}
bool TryGetSpawnPoint(out Vector2 spawnPoint)
{
spawnPoint = Vector2.Zero;
while (waypoints.Any())
{
var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
waypoints.Remove(wp);
if (!IsBlocked(wp.WorldPosition, paddedDimensions))
{
spawnPoint = wp.WorldPosition;
return true;
}
}
return false;
}
bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint)
{
// Shoot five rays and pick the highest hit point.
int rayCount = 5;
var positions = new Vector2[rayCount];
bool hit = false;
for (int i = 0; i < rayCount; i++)
{
float quarterWidth = subBorders.Width * 0.25f;
Vector2 rayStart = spawnPoint;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y);
break;
case 3:
rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y);
break;
case 4:
rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y);
break;
}
var simPos = ConvertUnits.ToSimUnits(rayStart);
var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1),
customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
if (body != null)
{
positions[i] =
ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) +
new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1));
hit = true;
}
}
float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y);
spawnPoint = new Vector2(spawnPoint.X, highestPoint);
return hit;
}
bool IsSideBlocked(Rectangle subBorders, bool front)
{
// Shoot three rays and check whether any of them hits.
int rayCount = 3;
Vector2 halfSize = subBorders.Size.ToVector2() / 2;
Vector2 quarterSize = halfSize / 2;
var positions = new Vector2[rayCount];
for (int i = 0; i < rayCount; i++)
{
float dir = front ? 1 : -1;
Vector2 rayStart;
Vector2 to;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
break;
case 0:
default:
rayStart = spawnPoint;
to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
break;
}
Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
customPredicate: f => f.Body?.UserData is VoronoiCell cell,
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
{
return true;
}
}
return false;
}
bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1)
{
float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared();
Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size);
if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds)))
{
return true;
}
if (Caves.Any(c =>
ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) ||
ToolBox.GetWorldBounds(c.StartPos, new Point(1500)).IntersectsWorld(bounds)))
{
return true;
}
return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
}
}
// For debugging
private readonly Dictionary<Submarine, List<Vector2>> wreckPositions = new Dictionary<Submarine, List<Vector2>>();
private readonly Dictionary<Submarine, List<Rectangle>> blockedRects = new Dictionary<Submarine, List<Rectangle>>();
private void CreateWrecks()
{
var totalSW = new Stopwatch();
totalSW.Start();
var wreckFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<WreckFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (wreckFiles.None())
{
DebugConsole.ThrowError("No wreck files found in the selected content packages!");
Wrecks = new List<Submarine>();
return;
}
wreckFiles.Shuffle(Rand.RandSync.ServerAndClient);
int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count);
int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count);
int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient);
if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false)
{
wreckCount = Math.Max(wreckCount, 1);
}
Wrecks = new List<Submarine>(wreckCount);
for (int i = 0; i < wreckCount; i++)
{
//how many times we'll try placing another sub before giving up
const int MaxSubsToTry = 2;
int attempts = 0;
while (wreckFiles.Any() && attempts < MaxSubsToTry)
{
ContentFile contentFile = wreckFiles.First();
wreckFiles.RemoveAt(0);
if (contentFile == null) { continue; }
string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null)
{
//placed successfully
break;
}
attempts++;
}
}
totalSW.Stop();
Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)");
}
private bool HasStartOutpost()
{
if (preSelectedStartOutpost != null) { return true; }
if (LevelData.Type != LevelData.LevelType.Outpost)
{
//only create a starting outpost in campaign and tutorial modes
#if CLIENT
if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible())
{
return false;
}
#else
if (!IsModeStartOutpostCompatible())
{
return false;
}
#endif
}
if (StartLocation != null && !StartLocation.Type.HasOutpost)
{
return false;
}
return true;
}
private bool HasEndOutpost()
{
if (preSelectedEndOutpost != null) { return true; }
//don't create an end outpost for locations
if (LevelData.Type == LevelData.LevelType.Outpost) { return false; }
if (EndLocation != null && !EndLocation.Type.HasOutpost) { return false; }
return true;
}
private void CreateOutposts()
{
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (!outpostFiles.Any() && !OutpostGenerationParams.OutpostParams.Any() && LevelData.ForceOutpostGenerationParams == null)
{
DebugConsole.ThrowError("No outpost files found in the selected content packages");
return;
}
for (int i = 0; i < 2; i++)
{
if (GameMain.GameSession?.GameMode is PvPMode) { continue; }
bool isStart = (i == 0) == !Mirrored;
if (isStart)
{
if (!HasStartOutpost()) { continue; }
}
else
{
if (!HasEndOutpost()) { continue; }
}
SubmarineInfo outpostInfo;
Submarine outpost = null;
if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null)
{
if (LevelData.OutpostGenerationParamsExist)
{
Location location = i == 0 ? StartLocation : EndLocation;
OutpostGenerationParams outpostGenerationParams = null;
if (LevelData.ForceOutpostGenerationParams != null)
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
else
{
outpostGenerationParams =
LevelData.ForceOutpostGenerationParams ??
LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
}
LocationType locationType = location?.Type;
if (locationType == null)
{
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
if (outpostGenerationParams.AllowedLocationTypes.Any())
{
locationType = LocationType.Prefabs.GetRandom(lt =>
outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType == "any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient);
}
}
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
else
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
foreach (string categoryToHide in locationType.HideEntitySubcategories)
{
foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false)))
{
entityToHide.HiddenInGame = true;
}
}
}
else
{
DebugConsole.NewMessage($"Loading a pre-built outpost for the {(isStart ? "start" : "end")} of the level...");
//backwards compatibility: if there are no generation params available, try to load an outpost file saved as a sub
ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient);
outpostInfo = new SubmarineInfo(outpostFile.Path.Value)
{
Type = SubmarineType.Outpost
};
outpost = new Submarine(outpostInfo);
}
}
else
{
DebugConsole.NewMessage($"Loading a pre-selected outpost for the {(isStart ? "start" : "end")} of the level...");
outpostInfo = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost;
outpostInfo.Type = SubmarineType.Outpost;
outpost = new Submarine(outpostInfo);
}
Point? minSize = null;
DockingPort subPort = null;
float closestDistance = float.MaxValue;
if (Submarine.MainSub != null)
{
Point subSize = Submarine.MainSub.GetDockedBorders().Size;
Point outpostSize = outpost.GetDockedBorders().Size;
minSize = new Point(Math.Max(subSize.X, outpostSize.X), subSize.Y + outpostSize.Y);
foreach (DockingPort port in DockingPort.List)
{
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine != Submarine.MainSub) { continue; }
//the submarine port has to be at the top of the sub
if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; }
float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X);
if (dist < closestDistance || subPort.MainDockingPort)
{
subPort = port;
closestDistance = dist;
}
}
}
Vector2 spawnPos;
if (GenerationParams.ForceOutpostPosition != Vector2.Zero)
{
spawnPos = new Vector2(Size.X * GenerationParams.ForceOutpostPosition.X, Size.Y * GenerationParams.ForceOutpostPosition.Y);
}
else
{
DockingPort outpostPort = null;
closestDistance = float.MaxValue;
foreach (DockingPort port in DockingPort.List)
{
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine != outpost) { continue; }
//the outpost port has to be at the bottom of the outpost
if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; }
float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X);
if (dist < closestDistance)
{
outpostPort = port;
closestDistance = dist;
}
}
float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X;
//don't try to compensate if the port is very far from the sub's center of mass
if (Math.Abs(subDockingPortOffset) > 5000.0f)
{
subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f);
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.";
DebugConsole.NewMessage(warningMsg, Color.Orange);
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
}
float? outpostDockingPortOffset = null;
if (outpostPort != null)
{
outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X;
//don't try to compensate if the port is very far from the outpost's center of mass
if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f)
{
outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f);
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.";
DebugConsole.NewMessage(warningMsg, Color.Orange);
GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg);
}
}
spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
if (Type == LevelData.LevelType.Outpost)
{
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
}
}
outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false);
foreach (WayPoint wp in WayPoint.WayPointList)
{
if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path)
{
PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost));
}
}
if ((i == 0) == !Mirrored)
{
StartOutpost = outpost;
if (StartLocation != null)
{
outpost.TeamID = StartLocation.Type.OutpostTeam;
outpost.Info.Name = StartLocation.Name;
}
}
else
{
EndOutpost = outpost;
if (EndLocation != null)
{
outpost.TeamID = EndLocation.Type.OutpostTeam;
outpost.Info.Name = EndLocation.Name;
}
}
}
}
private void CreateBeaconStation()
{
if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; }
var beaconStationFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<BeaconStationFile>())
.OrderBy(f => f.UintIdentifier).ToList();
if (beaconStationFiles.None())
{
DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!");
return;
}
var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
ContentFile contentFile = null;
if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation))
{
contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation);
if (contentFile == null)
{
DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead...");
}
}
if (contentFile == null)
{
for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
{
var beaconStationFile = beaconStationFiles[i];
var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
Debug.Assert(matchingInfo != null);
if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
{
if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
{
beaconStationFiles.RemoveAt(i);
}
}
}
if (beaconStationFiles.None())
{
DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
return;
}
contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
}
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation);
if (BeaconStation == null)
{
LevelData.HasBeaconStation = false;
return;
}
Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent<Sonar>() != null);
if (sonarItem == null)
{
DebugConsole.ThrowError($"No sonar found in the beacon station \"{beaconStationName}\"!");
return;
}
beaconSonar = sonarItem.GetComponent<Sonar>();
}
public void PrepareBeaconStation()
{
if (!LevelData.HasBeaconStation) { return; }
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
if (BeaconStation == null)
{
throw new InvalidOperationException("Failed to prepare beacon station (no beacon station in the level).");
}
List<Item> beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation);
Item reactorItem = beaconItems.Find(it => it.GetComponent<Reactor>() != null);
Reactor reactorComponent = null;
ItemContainer reactorContainer = null;
if (reactorItem != null)
{
reactorComponent = reactorItem.GetComponent<Reactor>();
reactorComponent.FuelConsumptionRate = 0.0f;
reactorContainer = reactorItem.GetComponent<ItemContainer>();
Repairable repairable = reactorItem.GetComponent<Repairable>();
if (repairable != null)
{
repairable.DeteriorationSpeed = 0.0f;
}
}
if (LevelData.IsBeaconActive)
{
if (reactorContainer != null && reactorContainer.Inventory.IsEmpty() &&
reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault()))
{
ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()];
Spawner.AddItemToSpawnQueue(
fuelPrefab, reactorContainer.Inventory,
onSpawned: (it) => reactorComponent.PowerUpImmediately());
}
beaconSonar.CurrentMode = Sonar.Mode.Active;
#if SERVER
beaconSonar.Item.CreateServerEvent(beaconSonar);
#endif
}
else
{
if (!(GameMain.NetworkMember?.IsClient ?? false))
{
bool allowDisconnectedWires = true;
bool allowDamagedWalls = true;
if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info)
{
allowDisconnectedWires = info.AllowDisconnectedWires;
allowDamagedWalls = info.AllowDamagedWalls;
}
//remove wires
float removeWireMinDifficulty = 20.0f;
float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
if (removeWireProbability > 0.0f && allowDisconnectedWires)
{
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
{
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
Wire wire = item.GetComponent<Wire>();
if (wire.Locked) { continue; }
if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent<ConnectionPanel>().Locked))
{
continue;
}
if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent<ConnectionPanel>().Locked))
{
continue;
}
if (Rand.Range(0f, 1.0f, Rand.RandSync.Unsynced) < removeWireProbability)
{
foreach (Connection connection in wire.Connections)
{
if (connection != null)
{
connection.ConnectionPanel.DisconnectedWires.Add(wire);
wire.RemoveConnection(connection.Item);
#if SERVER
connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
wire.CreateNetworkEvent();
#endif
}
}
}
}
}
if (allowDamagedWalls)
{
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
{
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
{
item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
}
}
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
{
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
{
int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
}
}
}
}
}
}
public bool CheckBeaconActive()
{
if (beaconSonar == null) { return false; }
return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active;
}
private bool IsModeStartOutpostCompatible()
{
#if CLIENT
return GameMain.GameSession?.GameMode is CampaignMode || GameMain.GameSession?.GameMode is TutorialMode || GameMain.GameSession?.GameMode is TestGameMode;
#else
return GameMain.GameSession?.GameMode is CampaignMode;
#endif
}
public void SpawnCorpses()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
foreach (Submarine wreck in Wrecks)
{
int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
pathPoints.Shuffle(Rand.RandSync.Unsynced);
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
var usedJobs = new HashSet<JobPrefab>();
int spawnCounter = 0;
for (int j = 0; j < corpseCount; j++)
{
WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault();
JobPrefab job = sp?.AssignedJob;
CorpsePrefab selectedPrefab;
if (job == null)
{
selectedPrefab = GetCorpsePrefab(usedJobs);
}
else
{
selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier);
if (selectedPrefab == null)
{
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
// Deduce the job from the selected prefab
selectedPrefab = GetCorpsePrefab(usedJobs);
if (selectedPrefab != null)
{
job = selectedPrefab.GetJobPrefab();
}
}
}
if (selectedPrefab == null) { continue; }
Vector2 worldPos;
if (sp == null)
{
if (!TryGetExtraSpawnPoint(out worldPos))
{
break;
}
}
else
{
worldPos = sp.WorldPosition;
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
}
job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
if (job == null) { continue; }
if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer")
{
// Only spawn one of these jobs per wreck
usedJobs.Add(job);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck, sp);
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
bool applyBurns = Rand.Value() < 0.1f;
bool applyDamage = Rand.Value() < 0.3f;
foreach (var limb in corpse.AnimController.Limbs)
{
if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
{
var prefab = AfflictionPrefab.BiteWounds;
float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
if (applyBurns)
{
var prefab = AfflictionPrefab.Burn;
float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
static float GetStrength(Limb limb, float max)
{
float strength = Rand.Range(0, max);
if (limb.type != LimbType.Head)
{
strength = Math.Min(strength, Rand.Range(0, max));
}
return strength;
}
}
corpse.CharacterHealth.ForceUpdateVisuals();
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
{
corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
}
spawnCounter++;
static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate = null)
{
IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(p =>
usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) &&
p.SpawnPosition == PositionType.Wreck &&
(predicate == null || predicate(p)));
return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced);
}
}
#if DEBUG
DebugConsole.NewMessage($"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow);
#endif
bool TryGetExtraSpawnPoint(out Vector2 point)
{
point = Vector2.Zero;
var hull = Hull.HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced();
if (hull != null)
{
point = hull.WorldPosition;
}
return hull != null;
}
}
}
public void SpawnNPCs()
{
if (Type != LevelData.LevelType.Outpost) { return; }
foreach (Submarine sub in Submarine.Loaded)
{
if (sub?.Info?.OutpostGenerationParams != null)
{
OutpostGenerator.SpawnNPCs((GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation, sub);
}
}
}
/// <summary>
/// Calculate the "real" depth in meters from the surface of Europa
/// </summary>
public float GetRealWorldDepth(float worldPositionY)
{
if (GameMain.GameSession?.Campaign == null)
{
//ensure the levels aren't too deep to traverse in non-campaign modes where you don't have the option to upgrade/switch the sub
return (-(worldPositionY - GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio;
}
else
{
return (-(worldPositionY - GenerationParams.Height) + LevelData.InitialDepth) * Physics.DisplayToRealWorldRatio;
}
}
public void DebugSetStartLocation(Location newStartLocation)
{
StartLocation = newStartLocation;
}
public void DebugSetEndLocation(Location newEndLocation)
{
EndLocation = newEndLocation;
}
public override void Remove()
{
base.Remove();
#if CLIENT
if (renderer != null)
{
renderer.Dispose();
renderer = null;
}
#endif
if (LevelObjectManager != null)
{
LevelObjectManager.Remove();
LevelObjectManager = null;
}
AbyssIslands?.Clear();
AbyssResources?.Clear();
Caves?.Clear();
Tunnels?.Clear();
PathPoints?.Clear();
PositionsOfInterest?.Clear();
wreckPositions?.Clear();
Wrecks?.Clear();
BeaconStation = null;
beaconSonar = null;
StartOutpost = null;
EndOutpost = null;
blockedRects?.Clear();
EntitiesBeforeGenerate?.Clear();
ClearEqualityCheckValues();
if (Ruins != null)
{
Ruins.Clear();
Ruins = null;
}
bottomPositions?.Clear();
BottomBarrier = null;
TopBarrier = null;
SeaFloor = null;
distanceField = null;
if (ExtraWalls != null)
{
foreach (LevelWall w in ExtraWalls) { w.Dispose(); }
ExtraWalls = null;
}
if (UnsyncedExtraWalls != null)
{
foreach (LevelWall w in UnsyncedExtraWalls) { w.Dispose(); }
UnsyncedExtraWalls = null;
}
tempCells?.Clear();
cells = null;
cellGrid = null;
if (bodies != null)
{
bodies.Clear();
bodies = null;
}
StartLocation = null;
EndLocation = null;
Loaded = null;
}
}
}