1070 lines
39 KiB
C#
1070 lines
39 KiB
C#
using Barotrauma.RuinGeneration;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Factories;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Voronoi2;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class Level
|
|
{
|
|
//all entities are disabled after they reach this depth
|
|
public const float MaxEntityDepth = -300000.0f;
|
|
public const float ShaftHeight = 1000.0f;
|
|
|
|
public static Level Loaded
|
|
{
|
|
get { return loaded; }
|
|
}
|
|
|
|
[Flags]
|
|
public enum PositionType
|
|
{
|
|
MainPath=1, Cave=2, Ruin=4
|
|
}
|
|
|
|
struct InterestingPosition
|
|
{
|
|
public readonly Vector2 Position;
|
|
public readonly PositionType PositionType;
|
|
|
|
public InterestingPosition(Vector2 position, PositionType positionType)
|
|
{
|
|
Position = position;
|
|
PositionType = positionType;
|
|
}
|
|
}
|
|
|
|
static Level loaded;
|
|
|
|
//how close the sub has to be to start/endposition to exit
|
|
public const float ExitDistance = 6000.0f;
|
|
|
|
private string seed;
|
|
|
|
public const int GridCellSize = 2000;
|
|
private List<VoronoiCell>[,] cellGrid;
|
|
|
|
private LevelWall[] extraWalls;
|
|
|
|
//private float shaftHeight;
|
|
|
|
//List<Body> bodies;
|
|
private List<VoronoiCell> cells;
|
|
|
|
//private VertexBuffer vertexBuffer;
|
|
|
|
private Vector2 startPosition, endPosition;
|
|
|
|
private Rectangle borders;
|
|
|
|
private List<Body> bodies;
|
|
|
|
private List<InterestingPosition> positionsOfInterest;
|
|
|
|
private List<Ruin> ruins;
|
|
|
|
private Color backgroundColor;
|
|
private Color wallColor;
|
|
|
|
private LevelGenerationParams generationParams;
|
|
|
|
private List<List<Vector2>> smallTunnels = new List<List<Vector2>>();
|
|
|
|
private static BackgroundSpriteManager backgroundSpriteManager;
|
|
|
|
public Vector2 StartPosition
|
|
{
|
|
get { return startPosition; }
|
|
}
|
|
|
|
public Vector2 Size
|
|
{
|
|
get { return new Vector2(borders.Width, borders.Height); }
|
|
}
|
|
|
|
public Vector2 EndPosition
|
|
{
|
|
get { return endPosition; }
|
|
}
|
|
|
|
public float BottomPos
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float SeaFloorTopPos
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public List<Ruin> Ruins
|
|
{
|
|
get { return ruins; }
|
|
}
|
|
|
|
public LevelWall[] ExtraWalls
|
|
{
|
|
get { return extraWalls; }
|
|
}
|
|
|
|
public List<List<Vector2>> SmallTunnels
|
|
{
|
|
get { return smallTunnels; }
|
|
}
|
|
|
|
public string Seed
|
|
{
|
|
get { return seed; }
|
|
}
|
|
|
|
public float Difficulty
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Body TopBarrier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Body BottomBarrier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
|
|
|
|
public LevelGenerationParams GenerationParams
|
|
{
|
|
get { return generationParams; }
|
|
}
|
|
|
|
public Color BackgroundColor
|
|
{
|
|
get { return backgroundColor; }
|
|
}
|
|
|
|
public Color WallColor
|
|
{
|
|
get { return wallColor; }
|
|
}
|
|
|
|
public Level(string seed, float difficulty, LevelGenerationParams generationParams)
|
|
{
|
|
this.seed = seed;
|
|
|
|
this.Difficulty = difficulty;
|
|
|
|
this.generationParams = generationParams;
|
|
|
|
borders = new Rectangle(0, 0, (int)generationParams.Width, (int)generationParams.Height);
|
|
}
|
|
|
|
public static Level CreateRandom(LocationConnection locationConnection)
|
|
{
|
|
string seed = locationConnection.Locations[0].Name + locationConnection.Locations[1].Name;
|
|
|
|
return new Level(seed, locationConnection.Difficulty, LevelGenerationParams.GetRandom(seed, locationConnection.Biome));
|
|
}
|
|
|
|
public static Level CreateRandom(string seed = "")
|
|
{
|
|
if (seed == "")
|
|
{
|
|
seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString();
|
|
}
|
|
|
|
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
|
|
|
return new Level(seed, Rand.Range(30.0f, 80.0f, Rand.RandSync.Server), LevelGenerationParams.GetRandom(seed));
|
|
}
|
|
|
|
public void Generate(bool mirror = false)
|
|
{
|
|
if (backgroundSpriteManager == null)
|
|
{
|
|
var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.BackgroundSpritePrefabs);
|
|
if (files.Count > 0)
|
|
backgroundSpriteManager = new BackgroundSpriteManager(files);
|
|
else
|
|
backgroundSpriteManager = new BackgroundSpriteManager("Content/BackgroundSprites/BackgroundSpritePrefabs.xml");
|
|
}
|
|
#if CLIENT
|
|
if (backgroundCreatureManager == null)
|
|
{
|
|
var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.BackgroundCreaturePrefabs);
|
|
if (files.Count > 0)
|
|
backgroundCreatureManager = new BackgroundCreatureManager(files);
|
|
else
|
|
backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundSprites/BackgroundCreaturePrefabs.xml");
|
|
}
|
|
#endif
|
|
|
|
Stopwatch sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
if (loaded != null) loaded.Unload();
|
|
loaded = this;
|
|
|
|
positionsOfInterest = new List<InterestingPosition>();
|
|
|
|
Voronoi voronoi = new Voronoi(1.0);
|
|
|
|
List<Vector2> sites = new List<Vector2>();
|
|
|
|
bodies = new List<Body>();
|
|
|
|
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
|
|
|
#if CLIENT
|
|
renderer = new LevelRenderer(this);
|
|
|
|
backgroundColor = generationParams.BackgroundColor;
|
|
float avgValue = (backgroundColor.R + backgroundColor.G + backgroundColor.G) / 3;
|
|
GameMain.LightManager.AmbientLight = new Color(backgroundColor * (10.0f / avgValue), 1.0f);
|
|
#endif
|
|
|
|
SeaFloorTopPos = generationParams.SeaFloorDepth + generationParams.MountainHeightMax + generationParams.SeaFloorVariance;
|
|
|
|
float minWidth = 6500.0f;
|
|
if (Submarine.MainSub != null)
|
|
{
|
|
Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders();
|
|
minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height));
|
|
}
|
|
|
|
Rectangle pathBorders = borders;
|
|
pathBorders.Inflate(-minWidth * 2, -minWidth * 2);
|
|
|
|
startPosition = new Vector2(
|
|
Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server),
|
|
Rand.Range(borders.Height * 0.5f, borders.Height - minWidth * 2, Rand.RandSync.Server));
|
|
|
|
endPosition = new Vector2(
|
|
borders.Width - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server),
|
|
Rand.Range(borders.Height * 0.5f, borders.Height - minWidth * 2, Rand.RandSync.Server));
|
|
|
|
//----------------------------------------------------------------------------------
|
|
//generate the initial nodes for the main path and smaller tunnels
|
|
//----------------------------------------------------------------------------------
|
|
|
|
List<Vector2> pathNodes = new List<Vector2>();
|
|
pathNodes.Add(new Vector2(startPosition.X, borders.Height));
|
|
|
|
Vector2 nodeInterval = generationParams.MainPathNodeIntervalRange;
|
|
|
|
for (float x = startPosition.X + Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server);
|
|
x < endPosition.X - Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server);
|
|
x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server))
|
|
{
|
|
pathNodes.Add(new Vector2(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server)));
|
|
}
|
|
|
|
pathNodes.Add(new Vector2(endPosition.X, borders.Height));
|
|
|
|
if (pathNodes.Count <= 2)
|
|
{
|
|
pathNodes.Add((startPosition + endPosition) / 2);
|
|
}
|
|
|
|
GenerateTunnels(pathNodes, minWidth);
|
|
|
|
//----------------------------------------------------------------------------------
|
|
//generate voronoi sites
|
|
//----------------------------------------------------------------------------------
|
|
|
|
Vector2 siteInterval = generationParams.VoronoiSiteInterval;
|
|
Vector2 siteVariance = generationParams.VoronoiSiteVariance;
|
|
for (float x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X)
|
|
{
|
|
for (float y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y)
|
|
{
|
|
Vector2 site = new Vector2(
|
|
x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server),
|
|
y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server));
|
|
|
|
if (smallTunnels.Any(t => t.Any(node => Vector2.Distance(node, site) < siteInterval.Length())))
|
|
{
|
|
//add some more sites around the small tunnels to generate more small voronoi cells
|
|
if (x < borders.Width - siteInterval.X) sites.Add(new Vector2(x, y) + Vector2.UnitX * siteInterval * 0.5f);
|
|
if (y < borders.Height - siteInterval.Y) sites.Add(new Vector2(x, y) + Vector2.UnitY * siteInterval * 0.5f);
|
|
if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y) sites.Add(new Vector2(x, y) + Vector2.One * siteInterval * 0.5f);
|
|
}
|
|
|
|
if (mirror) site.X = borders.Width - site.X;
|
|
|
|
sites.Add(site);
|
|
}
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// construct the voronoi graph and cells
|
|
//----------------------------------------------------------------------------------
|
|
|
|
Stopwatch sw2 = new Stopwatch();
|
|
sw2.Start();
|
|
|
|
List<GraphEdge> graphEdges = voronoi.MakeVoronoiGraph(sites, 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);
|
|
|
|
Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms");
|
|
sw2.Restart();
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// generate a path through the initial path nodes
|
|
//----------------------------------------------------------------------------------
|
|
|
|
List<VoronoiCell> mainPath = CaveGenerator.GeneratePath(pathNodes, cells, cellGrid, GridCellSize,
|
|
new Rectangle(pathBorders.X, pathBorders.Y, pathBorders.Width, borders.Height), 0.5f, mirror);
|
|
|
|
for (int i = 2; i < mainPath.Count; i += 3)
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(mainPath[i].Center, PositionType.MainPath));
|
|
}
|
|
|
|
List<VoronoiCell> pathCells = new List<VoronoiCell>(mainPath);
|
|
|
|
//make sure the path is wide enough to pass through
|
|
EnlargeMainPath(pathCells, minWidth);
|
|
|
|
foreach (InterestingPosition positionOfInterest in positionsOfInterest)
|
|
{
|
|
WayPoint wayPoint = new WayPoint(positionOfInterest.Position, SpawnType.Enemy, null);
|
|
wayPoint.MoveWithLevel = true;
|
|
}
|
|
|
|
startPosition.X = pathCells[0].Center.X;
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// tunnels through the tunnel nodes
|
|
//----------------------------------------------------------------------------------
|
|
|
|
foreach (List<Vector2> tunnel in smallTunnels)
|
|
{
|
|
if (tunnel.Count < 2) continue;
|
|
|
|
//find the cell which the path starts from
|
|
int startCellIndex = CaveGenerator.FindCellIndex(tunnel[0], cells, cellGrid, GridCellSize, 1);
|
|
if (startCellIndex < 0) continue;
|
|
|
|
//if it wasn't one of the cells in the main path, don't create a tunnel
|
|
if (cells[startCellIndex].CellType != CellType.Path) continue;
|
|
|
|
var newPathCells = CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders);
|
|
|
|
positionsOfInterest.Add(new InterestingPosition(tunnel.Last(), PositionType.Cave));
|
|
|
|
if (tunnel.Count > 4) positionsOfInterest.Add(new InterestingPosition(tunnel[tunnel.Count / 2], PositionType.Cave));
|
|
|
|
pathCells.AddRange(newPathCells);
|
|
}
|
|
|
|
Debug.WriteLine("path: " + sw2.ElapsedMilliseconds + " ms");
|
|
sw2.Restart();
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// remove unnecessary cells and create some holes at the bottom of the level
|
|
//----------------------------------------------------------------------------------
|
|
|
|
cells = CleanCells(pathCells);
|
|
pathCells.AddRange(CreateBottomHoles(generationParams.BottomHoleProbability, new Rectangle(
|
|
(int)(borders.Width * 0.2f), 0,
|
|
(int)(borders.Width * 0.6f), (int)(borders.Height * 0.8f))));
|
|
|
|
foreach (VoronoiCell cell in cells)
|
|
{
|
|
if (cell.Center.Y < borders.Height / 2) continue;
|
|
cell.edges.ForEach(e => e.OutsideLevel = true);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
foreach (VoronoiCell cell in cells)
|
|
{
|
|
int x = (int)Math.Floor(cell.Center.X / GridCellSize);
|
|
int y = (int)Math.Floor(cell.Center.Y / GridCellSize);
|
|
|
|
if (x < 0 || y < 0 || x >= cellGrid.GetLength(0) || y >= cellGrid.GetLength(1)) continue;
|
|
|
|
cellGrid[x, y].Add(cell);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// create some ruins
|
|
//----------------------------------------------------------------------------------
|
|
|
|
ruins = new List<Ruin>();
|
|
for (int i = 0; i < generationParams.RuinCount; i++)
|
|
{
|
|
GenerateRuin(mainPath);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// generate the bodies and rendered triangles of the cells
|
|
//----------------------------------------------------------------------------------
|
|
|
|
startPosition.Y = borders.Height;
|
|
endPosition.Y = borders.Height;
|
|
|
|
List<VoronoiCell> cellsWithBody = new List<VoronoiCell>(cells);
|
|
|
|
List<Vector2[]> triangles;
|
|
bodies = CaveGenerator.GeneratePolygons(cellsWithBody, out triangles);
|
|
|
|
#if CLIENT
|
|
renderer.SetBodyVertices(CaveGenerator.GenerateRenderVerticeList(triangles).ToArray(), generationParams.WallColor);
|
|
renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cells), generationParams.WallColor);
|
|
#endif
|
|
|
|
TopBarrier = BodyFactory.CreateEdge(GameMain.World,
|
|
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();
|
|
|
|
backgroundSpriteManager.PlaceSprites(this, generationParams.BackgroundSpriteAmount);
|
|
#if CLIENT
|
|
backgroundCreatureManager.SpawnSprites(80);
|
|
#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(null);
|
|
|
|
Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms");
|
|
sw2.Restart();
|
|
|
|
if (mirror)
|
|
{
|
|
Vector2 temp = startPosition;
|
|
startPosition = endPosition;
|
|
endPosition = temp;
|
|
}
|
|
|
|
Debug.WriteLine("**********************************************************************************");
|
|
Debug.WriteLine("Generated a map with " + sites.Count + " sites in " + sw.ElapsedMilliseconds + " ms");
|
|
Debug.WriteLine("Seed: " + seed);
|
|
Debug.WriteLine("**********************************************************************************");
|
|
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
DebugConsole.NewMessage("Generated level with the seed " + seed + " (type: " + generationParams.Name + ")", Color.White);
|
|
}
|
|
}
|
|
|
|
|
|
private List<VoronoiCell> CreateBottomHoles(float holeProbability, Rectangle limits)
|
|
{
|
|
List<VoronoiCell> toBeRemoved = new List<VoronoiCell>();
|
|
foreach (VoronoiCell cell in cells)
|
|
{
|
|
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) continue;
|
|
|
|
if (!limits.Contains(cell.Center)) continue;
|
|
|
|
float closestDist = 0.0f;
|
|
WayPoint closestWayPoint = null;
|
|
foreach (WayPoint wp in WayPoint.WayPointList)
|
|
{
|
|
if (wp.SpawnType != SpawnType.Path) continue;
|
|
|
|
float dist =Math.Abs(cell.Center.X - wp.WorldPosition.X);
|
|
if (closestWayPoint == null || dist < closestDist)
|
|
{
|
|
closestDist = dist;
|
|
closestWayPoint = wp;
|
|
}
|
|
}
|
|
|
|
if (closestWayPoint.WorldPosition.Y < cell.Center.Y) continue;
|
|
|
|
toBeRemoved.Add(cell);
|
|
}
|
|
|
|
return toBeRemoved;
|
|
}
|
|
|
|
private void EnlargeMainPath(List<VoronoiCell> pathCells, float minWidth)
|
|
{
|
|
List<WayPoint> wayPoints = new List<WayPoint>();
|
|
|
|
var newWaypoint = new WayPoint(new Rectangle((int)pathCells[0].Center.X, borders.Height, 10, 10), null);
|
|
newWaypoint.MoveWithLevel = true;
|
|
wayPoints.Add(newWaypoint);
|
|
|
|
for (int i = 0; i < pathCells.Count; i++)
|
|
{
|
|
pathCells[i].CellType = CellType.Path;
|
|
|
|
newWaypoint = new WayPoint(new Rectangle((int)pathCells[i].Center.X, (int)pathCells[i].Center.Y, 10, 10), null);
|
|
newWaypoint.MoveWithLevel = true;
|
|
wayPoints.Add(newWaypoint);
|
|
|
|
wayPoints[wayPoints.Count-2].linkedTo.Add(newWaypoint);
|
|
newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]);
|
|
|
|
for (int n = 0; n < wayPoints.Count; n++)
|
|
{
|
|
if (wayPoints[n].Position != newWaypoint.Position) continue;
|
|
|
|
wayPoints[n].linkedTo.Add(newWaypoint);
|
|
newWaypoint.linkedTo.Add(wayPoints[n]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
newWaypoint = new WayPoint(new Rectangle((int)pathCells[pathCells.Count - 1].Center.X, borders.Height, 10, 10), null);
|
|
newWaypoint.MoveWithLevel = true;
|
|
wayPoints.Add(newWaypoint);
|
|
|
|
wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint);
|
|
newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]);
|
|
|
|
if (minWidth > 0.0f)
|
|
{
|
|
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 List<VoronoiCell> GetTooCloseCells(List<VoronoiCell> emptyCells, float minDistance)
|
|
{
|
|
List<VoronoiCell> tooCloseCells = new List<VoronoiCell>();
|
|
|
|
Vector2 position = emptyCells[0].Center;
|
|
|
|
if (minDistance == 0.0f) return tooCloseCells;
|
|
|
|
float step = 100.0f;
|
|
|
|
int targetCellIndex = 1;
|
|
|
|
minDistance *= 0.5f;
|
|
do
|
|
{
|
|
tooCloseCells.AddRange(GetTooCloseCells(position, minDistance));
|
|
|
|
position += Vector2.Normalize(emptyCells[targetCellIndex].Center - position) * step;
|
|
|
|
if (Vector2.Distance(emptyCells[targetCellIndex].Center, position) < step * 2.0f) targetCellIndex++;
|
|
|
|
} while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f);
|
|
|
|
return tooCloseCells;
|
|
}
|
|
|
|
private List<VoronoiCell> GetTooCloseCells(Vector2 position, float minDistance)
|
|
{
|
|
List<VoronoiCell> tooCloseCells = new List<VoronoiCell>();
|
|
|
|
var closeCells = GetCells(position, 3);
|
|
|
|
foreach (VoronoiCell cell in closeCells)
|
|
{
|
|
bool tooClose = false;
|
|
foreach (GraphEdge edge in cell.edges)
|
|
{
|
|
|
|
if (Vector2.Distance(edge.point1, position) < minDistance ||
|
|
Vector2.Distance(edge.point2, position) < minDistance)
|
|
{
|
|
tooClose = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tooClose && !tooCloseCells.Contains(cell)) tooCloseCells.Add(cell);
|
|
}
|
|
|
|
return tooCloseCells;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// remove all cells except those that are adjacent to the empty cells
|
|
/// </summary>
|
|
private List<VoronoiCell> CleanCells(List<VoronoiCell> emptyCells)
|
|
{
|
|
List<VoronoiCell> newCells = new List<VoronoiCell>();
|
|
|
|
foreach (VoronoiCell cell in emptyCells)
|
|
{
|
|
foreach (GraphEdge edge in cell.edges)
|
|
{
|
|
VoronoiCell adjacent = edge.AdjacentCell(cell);
|
|
if (adjacent != null && !newCells.Contains(adjacent))
|
|
{
|
|
newCells.Add(adjacent);
|
|
}
|
|
}
|
|
}
|
|
|
|
return newCells;
|
|
}
|
|
|
|
private void GenerateSeaFloor()
|
|
{
|
|
BottomPos = generationParams.SeaFloorDepth;
|
|
SeaFloorTopPos = BottomPos;
|
|
|
|
List<Vector2> bottomPositions = new List<Vector2>();
|
|
bottomPositions.Add(new Vector2(0, BottomPos));
|
|
|
|
int mountainCount = Rand.Range(generationParams.MountainCountMin, generationParams.MountainCountMax, Rand.RandSync.Server);
|
|
for (int i = 0; i < mountainCount; i++)
|
|
{
|
|
bottomPositions.Add(
|
|
new Vector2(Size.X / (mountainCount + 1) * (i + 1),
|
|
BottomPos + Rand.Range(generationParams.MountainHeightMin, generationParams.MountainHeightMax, Rand.RandSync.Server)));
|
|
}
|
|
bottomPositions.Add(new Vector2(Size.X, BottomPos));
|
|
|
|
float minVertexInterval = 5000.0f;
|
|
float currInverval = Size.X / 2.0f;
|
|
while (currInverval > minVertexInterval)
|
|
{
|
|
for (int i = 0; i < bottomPositions.Count - 1; i++)
|
|
{
|
|
bottomPositions.Insert(i+1,
|
|
(bottomPositions[i] + bottomPositions[i + 1]) / 2.0f +
|
|
Vector2.UnitY * Rand.Range(0.0f, generationParams.SeaFloorVariance, Rand.RandSync.Server));
|
|
|
|
i++;
|
|
}
|
|
|
|
currInverval /= 2.0f;
|
|
}
|
|
|
|
SeaFloorTopPos = bottomPositions.Max(p => p.Y);
|
|
|
|
extraWalls = new LevelWall[] { new LevelWall(bottomPositions, new Vector2(0.0f, -2000.0f), backgroundColor) };
|
|
|
|
BottomBarrier = BodyFactory.CreateEdge(GameMain.World,
|
|
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 GenerateTunnels(List<Vector2> pathNodes, float pathWidth)
|
|
{
|
|
smallTunnels = new List<List<Vector2>>();
|
|
for (int i = 0; i < generationParams.SmallTunnelCount; i++)
|
|
{
|
|
int startNodeIndex = Rand.Range(1, pathNodes.Count - 2, Rand.RandSync.Server);
|
|
var tunnelStartPos = Vector2.Lerp(pathNodes[startNodeIndex], pathNodes[startNodeIndex + 1], Rand.Range(0.0f, 1.0f, Rand.RandSync.Server));
|
|
|
|
float tunnelLength = Rand.Range(
|
|
generationParams.SmallTunnelLengthRange.X,
|
|
generationParams.SmallTunnelLengthRange.Y,
|
|
Rand.RandSync.Server);
|
|
|
|
List<Vector2> tunnelNodes = new List<Vector2>()
|
|
{
|
|
tunnelStartPos,
|
|
tunnelStartPos + Vector2.UnitY * Math.Sign(tunnelStartPos.Y - Size.Y / 2) * pathWidth * 2
|
|
};
|
|
|
|
List<Vector2> tunnel = GenerateTunnel(
|
|
tunnelNodes,
|
|
Rand.Range(generationParams.SmallTunnelLengthRange.X, generationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server),
|
|
pathNodes);
|
|
if (tunnel.Any()) smallTunnels.Add(tunnel);
|
|
|
|
int branches = Rand.Range(0, 3, Rand.RandSync.Server);
|
|
for (int j = 0; j < branches; j++)
|
|
{
|
|
List<Vector2> branch = GenerateTunnel(
|
|
new List<Vector2>() { tunnel[Rand.Int(tunnel.Count, Rand.RandSync.Server)] },
|
|
Rand.Range(generationParams.SmallTunnelLengthRange.X, generationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server) * 0.5f,
|
|
pathNodes);
|
|
if (branch.Any()) smallTunnels.Add(branch);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private List<Vector2> GenerateTunnel(List<Vector2> tunnelNodes, float tunnelLength, List<Vector2> avoidNodes)
|
|
{
|
|
float sectionLength = 1000.0f;
|
|
|
|
float currLength = 0.0f;
|
|
while (currLength < tunnelLength)
|
|
{
|
|
Vector2 dir = Rand.Vector(1.0f, Rand.RandSync.Server);
|
|
|
|
dir.Y += Math.Sign(tunnelNodes[tunnelNodes.Count - 1].Y - Size.Y / 2) * 0.5f;
|
|
if (tunnelNodes.Count > 1)
|
|
{
|
|
//keep heading roughly in the same direction as the previous nodes
|
|
Vector2 prevNodeDiff = tunnelNodes[tunnelNodes.Count - 1] - tunnelNodes[tunnelNodes.Count - 2];
|
|
if (prevNodeDiff != Vector2.Zero)
|
|
{
|
|
dir += Vector2.Normalize(tunnelNodes[tunnelNodes.Count - 1] - tunnelNodes[tunnelNodes.Count - 2]) * 0.5f;
|
|
}
|
|
}
|
|
|
|
float avoidDist = 20000.0f;
|
|
foreach (Vector2 pathNode in avoidNodes)
|
|
{
|
|
Vector2 diff = tunnelNodes[tunnelNodes.Count - 1] - pathNode;
|
|
if (diff == Vector2.Zero) continue;
|
|
|
|
float dist = diff.Length();
|
|
if (dist < avoidDist)
|
|
{
|
|
dir += (diff / dist) * (1.0f - dist / avoidDist);
|
|
}
|
|
}
|
|
|
|
Vector2 normalizedDir = Vector2.Normalize(dir);
|
|
|
|
if (tunnelNodes.Last().Y + normalizedDir.Y > Size.Y)
|
|
{
|
|
//head back down if the tunnel has reached the top of the level
|
|
normalizedDir.Y = -normalizedDir.Y;
|
|
}
|
|
else if (tunnelNodes.Last().Y + normalizedDir.Y + normalizedDir.Y < 0.0f ||
|
|
tunnelNodes.Last().Y + normalizedDir.Y + normalizedDir.Y < SeaFloorTopPos)
|
|
{
|
|
//head back up if reached the sea floor
|
|
normalizedDir.Y = -normalizedDir.Y;
|
|
}
|
|
|
|
Vector2 nextNode = tunnelNodes.Last() + normalizedDir * sectionLength;
|
|
|
|
nextNode.X = MathHelper.Clamp(nextNode.X, 500.0f, Size.X - 500.0f);
|
|
nextNode.Y = MathHelper.Clamp(nextNode.Y, SeaFloorTopPos, Size.Y - 500.0f);
|
|
tunnelNodes.Add(nextNode);
|
|
currLength += sectionLength;
|
|
}
|
|
|
|
return tunnelNodes;
|
|
}
|
|
|
|
private void GenerateRuin(List<VoronoiCell> mainPath)
|
|
{
|
|
Vector2 ruinSize = new Vector2(Rand.Range(5000.0f, 8000.0f, Rand.RandSync.Server), Rand.Range(5000.0f, 8000.0f, Rand.RandSync.Server));
|
|
float ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) * 0.5f;
|
|
|
|
Vector2 ruinPos = cells[Rand.Int(cells.Count, Rand.RandSync.Server)].Center;
|
|
|
|
//50% chance of placing the ruins at a cave
|
|
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f)
|
|
{
|
|
TryGetInterestingPosition(true, PositionType.Cave, 0.0f, out ruinPos);
|
|
}
|
|
|
|
ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2);
|
|
ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2.0f);
|
|
|
|
int iter = 0;
|
|
while (mainPath.Any(p => Vector2.Distance(ruinPos, p.Center) < ruinRadius * 2.0f))
|
|
{
|
|
Vector2 weighedPathPos = ruinPos;
|
|
iter++;
|
|
|
|
foreach (VoronoiCell pathCell in mainPath)
|
|
{
|
|
float dist = Vector2.Distance(pathCell.Center, ruinPos);
|
|
if (dist > 10000.0f) continue;
|
|
|
|
Vector2 moveAmount = Vector2.Normalize(ruinPos - pathCell.Center) * 100000.0f / dist;
|
|
|
|
weighedPathPos += moveAmount;
|
|
weighedPathPos.Y = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPos.Y);
|
|
}
|
|
|
|
ruinPos = weighedPathPos;
|
|
|
|
if (iter > 10000) break;
|
|
}
|
|
|
|
VoronoiCell closestPathCell = null;
|
|
float closestDist = 0.0f;
|
|
foreach (VoronoiCell pathCell in mainPath)
|
|
{
|
|
float dist = Vector2.Distance(pathCell.Center, ruinPos);
|
|
if (closestPathCell == null || dist < closestDist)
|
|
{
|
|
closestPathCell = pathCell;
|
|
closestDist = dist;
|
|
}
|
|
}
|
|
|
|
var ruin = new Ruin(closestPathCell, cells, new Rectangle(MathUtils.ToPoint(ruinPos - ruinSize * 0.5f), MathUtils.ToPoint(ruinSize)));
|
|
ruins.Add(ruin);
|
|
|
|
ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance));
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center.ToVector2(), PositionType.Ruin));
|
|
}
|
|
|
|
foreach (RuinShape ruinShape in ruin.RuinShapes)
|
|
{
|
|
var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height));
|
|
|
|
foreach (VoronoiCell cell in tooClose)
|
|
{
|
|
if (cell.CellType == CellType.Empty) continue;
|
|
foreach (GraphEdge e in cell.edges)
|
|
{
|
|
Rectangle rect = ruinShape.Rect;
|
|
rect.Y += rect.Height;
|
|
if (ruinShape.Rect.Contains(e.point1) || ruinShape.Rect.Contains(e.point2) ||
|
|
MathUtils.GetLineRectangleIntersection(e.point1, e.point2, rect) != null)
|
|
{
|
|
cell.CellType = CellType.Removed;
|
|
|
|
int x = (int)Math.Floor(cell.Center.X / GridCellSize);
|
|
int y = (int)Math.Floor(cell.Center.Y / GridCellSize);
|
|
|
|
cellGrid[x, y].Remove(cell);
|
|
cells.Remove(cell);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f)
|
|
{
|
|
if (!positionsOfInterest.Any()) return Size*0.5f;
|
|
|
|
Vector2 position = Vector2.Zero;
|
|
|
|
offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall);
|
|
|
|
int tries = 0;
|
|
do
|
|
{
|
|
Vector2 startPos;
|
|
Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out startPos);
|
|
|
|
startPos += Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server);
|
|
|
|
Vector2 endPos = startPos - Vector2.UnitY * Size.Y;
|
|
|
|
if (Submarine.PickBody(
|
|
ConvertUnits.ToSimUnits(startPos),
|
|
ConvertUnits.ToSimUnits(endPos),
|
|
null, Physics.CollisionLevel) != null)
|
|
{
|
|
position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos)*offsetFromWall;
|
|
break;
|
|
}
|
|
|
|
tries++;
|
|
|
|
if (tries == 10)
|
|
{
|
|
position = EndPosition - Vector2.UnitY * 300.0f;
|
|
}
|
|
|
|
} while (tries < 10);
|
|
|
|
return position;
|
|
}
|
|
|
|
|
|
|
|
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position)
|
|
{
|
|
if (!positionsOfInterest.Any())
|
|
{
|
|
position = Size * 0.5f;
|
|
return false;
|
|
}
|
|
|
|
var matchingPositions = positionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
|
|
|
|
if (minDistFromSubs > 0.0f)
|
|
{
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
matchingPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position, sub.WorldPosition) < minDistFromSubs * minDistFromSubs);
|
|
}
|
|
}
|
|
|
|
if (!matchingPositions.Any())
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError("Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + "\n" + Environment.StackTrace);
|
|
#endif
|
|
|
|
position = positionsOfInterest[Rand.Int(positionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
|
|
return false;
|
|
}
|
|
|
|
position = matchingPositions[Rand.Int(matchingPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
|
|
return true;
|
|
}
|
|
|
|
public void Update(float deltaTime, Camera cam)
|
|
{
|
|
backgroundSpriteManager.Update(deltaTime);
|
|
|
|
#if CLIENT
|
|
backgroundCreatureManager.Update(deltaTime, cam);
|
|
|
|
if (Hull.renderer != null)
|
|
{
|
|
Hull.renderer.ScrollWater((float)deltaTime);
|
|
}
|
|
|
|
renderer.Update(deltaTime);
|
|
#endif
|
|
}
|
|
|
|
public List<VoronoiCell> GetCells(Vector2 pos, int searchDepth = 2)
|
|
{
|
|
int gridPosX = (int)Math.Floor(pos.X / GridCellSize);
|
|
int gridPosY = (int)Math.Floor(pos.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);
|
|
|
|
List<VoronoiCell> cells = new List<VoronoiCell>();
|
|
|
|
for (int x = startX; x <= endX; x++)
|
|
{
|
|
for (int y = startY; y <= endY; y++)
|
|
{
|
|
foreach (VoronoiCell cell in cellGrid[x, y])
|
|
{
|
|
cells.Add(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (extraWalls != null)
|
|
{
|
|
Debug.Assert(extraWalls.Count() == 1, "Level.GetCells may need to be updated to support extra walls other than the ocean floor.");
|
|
if (pos.Y - searchDepth * GridCellSize < SeaFloorTopPos)
|
|
{
|
|
foreach (VoronoiCell cell in extraWalls[0].Cells)
|
|
{
|
|
if (Math.Abs(cell.Center.X - pos.X) < searchDepth * GridCellSize)
|
|
{
|
|
cells.Add(cell);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return cells;
|
|
}
|
|
|
|
private void Unload()
|
|
{
|
|
#if CLIENT
|
|
if (renderer != null)
|
|
{
|
|
renderer.Dispose();
|
|
renderer = null;
|
|
}
|
|
#endif
|
|
|
|
if (ruins != null)
|
|
{
|
|
ruins.Clear();
|
|
ruins = null;
|
|
}
|
|
|
|
|
|
if (extraWalls != null)
|
|
{
|
|
foreach (LevelWall w in extraWalls)
|
|
{
|
|
w.Dispose();
|
|
}
|
|
|
|
extraWalls = null;
|
|
}
|
|
|
|
cells = null;
|
|
|
|
if (bodies != null)
|
|
{
|
|
bodies.Clear();
|
|
bodies = null;
|
|
}
|
|
|
|
loaded = null;
|
|
}
|
|
|
|
}
|
|
|
|
}
|