15a31c5291
- The sub can be moved from location to another in the map view by double clicking in debug builds. - Level wall color can be changed in level generation parameters. - Fixed level geometry not being rendered if the ocean floor is visible (which isn't a problem in most level types, but there can be levels where the ocean floor is so close to the actual level that they can both be visible at the same time). - Background sprite scale is taken into account when calculating particle emitter positions. - Fixed limb lights being rendered even if the character is disabled.
977 lines
34 KiB
C#
977 lines
34 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;
|
|
|
|
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 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)
|
|
{
|
|
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
|
|
|
|
float minWidth = 6500.0f;
|
|
if (Submarine.MainSub != null)
|
|
{
|
|
Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders();
|
|
minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height));
|
|
}
|
|
|
|
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));
|
|
|
|
List<Vector2> pathNodes = new List<Vector2>();
|
|
Rectangle pathBorders = borders;// new Rectangle((int)minWidth, (int)minWidth, borders.Width - (int)minWidth * 2, borders.Height - (int)minWidth);
|
|
pathBorders.Inflate(-minWidth*2, -minWidth*2);
|
|
|
|
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);
|
|
}
|
|
|
|
List<List<Vector2>> smallTunnels = new List<List<Vector2>>();
|
|
for (int i = 0; i < generationParams.SmallTunnelCount; i++)
|
|
{
|
|
var tunnelStartPos = pathNodes[Rand.Range(2, pathNodes.Count - 2, Rand.RandSync.Server)];
|
|
tunnelStartPos.X = MathHelper.Clamp(tunnelStartPos.X, pathBorders.X, pathBorders.Right);
|
|
|
|
float tunnelLength = Rand.Range(
|
|
generationParams.SmallTunnelLengthRange.X,
|
|
generationParams.SmallTunnelLengthRange.Y,
|
|
Rand.RandSync.Server);
|
|
|
|
var tunnelNodes = MathUtils.GenerateJaggedLine(
|
|
tunnelStartPos,
|
|
new Vector2(tunnelStartPos.X, pathBorders.Bottom)+Rand.Vector(tunnelLength, Rand.RandSync.Server),
|
|
4, 1000.0f);
|
|
|
|
List<Vector2> tunnel = new List<Vector2>();
|
|
foreach (Vector2[] tunnelNode in tunnelNodes)
|
|
{
|
|
if (!pathBorders.Contains(tunnelNode[0])) continue;
|
|
tunnel.Add(tunnelNode[0]);
|
|
}
|
|
|
|
if (tunnel.Any()) smallTunnels.Add(tunnel);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
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();
|
|
|
|
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);
|
|
}
|
|
|
|
foreach (VoronoiCell cell in pathCells)
|
|
{
|
|
cell.edges.ForEach(e => e.OutsideLevel = false);
|
|
|
|
cell.CellType = CellType.Path;
|
|
cells.Remove(cell);
|
|
}
|
|
|
|
//generate some narrow caves
|
|
int caveAmount = 0;// Rand.Int(3, false);
|
|
List<VoronoiCell> usedCaveCells = new List<VoronoiCell>();
|
|
for (int i = 0; i < caveAmount; i++)
|
|
{
|
|
Vector2 startPoint = Vector2.Zero;
|
|
VoronoiCell startCell = null;
|
|
|
|
var caveCells = new List<VoronoiCell>();
|
|
|
|
int maxTries = 5, tries = 0;
|
|
while (tries<maxTries)
|
|
{
|
|
startCell = cells[Rand.Int(cells.Count, Rand.RandSync.Server)];
|
|
|
|
//find an edge between the cell and the already carved path
|
|
GraphEdge startEdge =
|
|
startCell.edges.Find(e => pathCells.Contains(e.AdjacentCell(startCell)));
|
|
|
|
if (startEdge != null)
|
|
{
|
|
startPoint = (startEdge.point1 + startEdge.point2) / 2.0f;
|
|
startPoint += startPoint - startCell.Center;
|
|
|
|
//get the cells in which the cave will be carved
|
|
caveCells = GetCells(startCell.Center, 2);
|
|
//remove cells that have already been "carved" out
|
|
caveCells.RemoveAll(c => c.CellType == CellType.Path);
|
|
|
|
//if any of the cells have already been used as a cave, continue and find some other cells
|
|
if (usedCaveCells.Any(c => caveCells.Contains(c))) continue;
|
|
break;
|
|
}
|
|
|
|
tries++;
|
|
}
|
|
|
|
//couldn't find a place for a cave -> abort
|
|
if (tries >= maxTries) break;
|
|
|
|
if (!caveCells.Any()) continue;
|
|
|
|
usedCaveCells.AddRange(caveCells);
|
|
|
|
List<VoronoiCell> caveSolidCells;
|
|
var cavePathCells = CaveGenerator.CarveCave(caveCells, startPoint, out caveSolidCells);
|
|
|
|
//remove the large cells used as a "base" for the cave (they've now been replaced with smaller ones)
|
|
caveCells.ForEach(c => cells.Remove(c));
|
|
|
|
cells.AddRange(caveSolidCells);
|
|
|
|
foreach (VoronoiCell cell in cavePathCells)
|
|
{
|
|
cells.Remove(cell);
|
|
}
|
|
|
|
pathCells.AddRange(cavePathCells);
|
|
|
|
for (int j = cavePathCells.Count / 2; j < cavePathCells.Count; j += 10)
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(cavePathCells[j].Center, PositionType.Cave));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
ruins = new List<Ruin>();
|
|
for (int i = 0; i < generationParams.RuinCount; i++)
|
|
{
|
|
GenerateRuin(mainPath);
|
|
}
|
|
|
|
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();
|
|
|
|
#if CLIENT
|
|
renderer.PlaceSprites(generationParams.BackgroundSpriteAmount);
|
|
#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("**********************************************************************************");
|
|
}
|
|
|
|
|
|
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 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;
|
|
|
|
int iter = 0;
|
|
|
|
ruinPos.Y = Math.Min(borders.Y + borders.Height - ruinSize.Y/2, ruinPos.Y);
|
|
|
|
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;
|
|
|
|
//if (weighedPathPos.Y + moveAmount.Y > borders.Bottom - ruinSize.X)
|
|
//{
|
|
// moveAmount.X = (Math.Abs(moveAmount.Y) + Math.Abs(moveAmount.X))*Math.Sign(moveAmount.X);
|
|
// moveAmount.Y = 0.0f;
|
|
//}
|
|
|
|
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 (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 offsetFromWall = 10.0f)
|
|
{
|
|
if (!positionsOfInterest.Any()) return Size*0.5f;
|
|
|
|
Vector2 position = Vector2.Zero;
|
|
|
|
offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall);
|
|
|
|
int tries = 0;
|
|
do
|
|
{
|
|
Vector2 startPos = Level.Loaded.GetRandomInterestingPosition(true, spawnPosType, true);
|
|
|
|
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 Vector2 GetRandomInterestingPosition(bool useSyncedRand, PositionType positionType, bool avoidSubs)
|
|
{
|
|
if (!positionsOfInterest.Any()) return Size * 0.5f;
|
|
|
|
var matchingPositions = positionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
|
|
|
|
if (avoidSubs)
|
|
{
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
float minDist = Math.Max(sub.Borders.Width, sub.Borders.Height);
|
|
matchingPositions.RemoveAll(p => Vector2.Distance(p.Position, sub.WorldPosition) < minDist);
|
|
}
|
|
}
|
|
|
|
if (!matchingPositions.Any())
|
|
{
|
|
return positionsOfInterest[Rand.Int(positionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
|
|
}
|
|
|
|
return matchingPositions[Rand.Int(matchingPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position;
|
|
}
|
|
|
|
public void Update(float deltaTime)
|
|
{
|
|
/*
|
|
if (Submarine.MainSub != null)
|
|
{
|
|
WrappingWall.UpdateWallShift(Submarine.MainSub.WorldPosition, wrappingWalls);
|
|
}*/
|
|
|
|
#if CLIENT
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
}
|