74086415fc
commit e08c5e722fdff38d408428d138919bb8ea90321b Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Feb 5 11:13:25 2019 +0200 Changed default radio chat keybind to T. OemTilde works as an apostrophe on some keyboard layouts, making it difficult to type in chat messages without deselecting the chatbox. Closes #1044 commit e0ca9ffebe06b3f711b28a3ccfd486d84c47f9b0 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Tue Feb 5 11:03:14 2019 +0200 Fixed content package compatibility check. Closes #1059 commit f54bc180e8aef383cf9c893cd44c103840c066ac Author: itchyOwl <lauri.harkanen@gmail.com> Date: Tue Feb 5 10:48:03 2019 +0200 Remove the spacing from the crew selection listbox and disable focusing from the background. An alternative fix for #1039. commit d037815054ecc8269b0af3297700d2b98e1e5bc5 Author: itchyOwl <lauri.harkanen@gmail.com> Date: Tue Feb 5 10:40:52 2019 +0200 Enforce vsync during the splash screen. Should fix the crashes due to insufficient memory mentioned in #1060. Does not fix the issue on releasing the used video resources. commit 90f1d8fe168356ba3c797c0c58fadea2aff57458 Author: ezjamsen <ezjames.fi@gmail.com> Date: Mon Feb 4 23:05:54 2019 +0200 small updates to subs commit 56dbdf6ea6e8e14035f943eb232eed7aa250f8f6 Author: ezjamsen <ezjames.fi@gmail.com> Date: Mon Feb 4 23:05:26 2019 +0200 adjusted default deterioration values on prefabs commit eaa4818cb63d52e1af519c8ca5c4f01f20143cb2 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 21:59:03 2019 +0200 Fixed servers occasionally starting the round multiple times when automatically starting the game via autorestart or clients being ready. Happened because "initiatedStartGame" (which prevents starting the round again) was not set until the InitiateStartGame coroutine is run for the first time, causing the automatic restart logic to run one extra time. Closes #1056 commit afd83b6ef40333a95e5eeec400d5f981cbdb1889 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 21:36:35 2019 +0200 Fixed some typos in affliction & mission descriptions. Closes #1057 commit feb97628582a9a22f0660a8ab2798f8e03fe8fc9 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 18:29:40 2019 +0200 Fixes to Level.TryGetInterestingPosition. Should fix monsters spawning very close to the sub in some levels (see #1054). - Ignore outposts when determining which positions are far enough from the sub. - If no position far enough from the sub is found, use the furthest one instead of choosing randomly. commit 62e5d7b229f334a5cf15cedf22e6795f18d637b9 Merge: 6ffc90da6 73f3dd060 Author: itchyOwl <lauri.harkanen@gmail.com> Date: Mon Feb 4 18:03:44 2019 +0200 Merge branch 'dev' of https://github.com/Regalis11/Barotrauma into dev commit 6ffc90da69d9a5db146435054fdbb2cb18d855be Author: itchyOwl <lauri.harkanen@gmail.com> Date: Mon Feb 4 18:03:26 2019 +0200 Allow to focus on the crew selection listbox to enable scrolling when the mouse cursor is not over a content element, but is inside the listbox. Another option would be to remove the spacing from the listbox. Fixes #1039. commit 73f3dd060ef208199941cb74a72dcda6a7c15f41 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 18:00:57 2019 +0200 Fixed compiler error in GameServer.ClientWriteIngame commit 131263490c7093dd526a84757f4edd096a213a74 Merge: 637668488 9753e46a5 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 17:55:39 2019 +0200 Merge branch 'dev' of https://github.com/Regalis11/Barotrauma into dev commit 637668488b27954afa0a8485eeaa3b23cf9461c5 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 17:55:01 2019 +0200 Changed character culling logic a bit. The characters need to be 22,000 units away from the players to be disabled, and won't be re-enabled until they're within 20,000 units. Using 20,000 as the threshold for both caused characters to be constantly toggled on/off when they're around the maximum distance, which caused ContactManager to constantly have to update contacts. commit 9753e46a5694b7b3c2bd0c4c006e2c1856c6eddb Author: itchyOwl <lauri.harkanen@gmail.com> Date: Mon Feb 4 17:54:35 2019 +0200 Add a debug command for adding more money in the compaign mode. commit f8a7179d6f8cb7c834b91b942722b1ae05417316 Author: itchyOwl <lauri.harkanen@gmail.com> Date: Mon Feb 4 17:53:03 2019 +0200 Always disallow zooming when the cursor is over a gui element. commit eefd956c50650be906acbd10662e24507e48cb83 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Feb 4 17:47:18 2019 +0200 Don't display outpost markers on the sonar (the ending and starting positions of the level are displayed, no need to show an additional marker on the "outpost submarine"). commit c7883bafc634e0ac2603bc96a87433da0c3481df Author: ezjamsen <ezjames.fi@gmail.com> Date: Mon Feb 4 17:21:50 2019 +0200 removed references to medical syringes
1601 lines
64 KiB
C#
1601 lines
64 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.Networking;
|
|
using Barotrauma.RuinGeneration;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Factories;
|
|
using Lidgren.Network;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Voronoi2;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class Level : Entity, IServerSerializable
|
|
{
|
|
//all entities are disabled after they reach this depth
|
|
public const int MaxEntityDepth = -300000;
|
|
public const float ShaftHeight = 1000.0f;
|
|
|
|
public static Level Loaded
|
|
{
|
|
get { return loaded; }
|
|
}
|
|
|
|
[Flags]
|
|
public enum PositionType
|
|
{
|
|
MainPath = 1, Cave = 2, Ruin = 4
|
|
}
|
|
|
|
public struct InterestingPosition
|
|
{
|
|
public Point Position;
|
|
public readonly PositionType PositionType;
|
|
|
|
public InterestingPosition(Point 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 List<LevelWall> extraWalls;
|
|
|
|
private LevelWall seaFloor;
|
|
|
|
private List<VoronoiCell> cells;
|
|
|
|
private Point startPosition, endPosition;
|
|
|
|
private Rectangle borders;
|
|
|
|
private List<Body> bodies;
|
|
|
|
private List<InterestingPosition> positionsOfInterest;
|
|
|
|
private List<Ruin> ruins;
|
|
|
|
private LevelGenerationParams generationParams;
|
|
|
|
private List<List<Point>> smallTunnels = new List<List<Point>>();
|
|
|
|
private LevelObjectManager levelObjectManager;
|
|
|
|
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(); }
|
|
}
|
|
|
|
public Point Size
|
|
{
|
|
get { return new Point(borders.Width, borders.Height); }
|
|
}
|
|
|
|
public Vector2 EndPosition
|
|
{
|
|
get { return endPosition.ToVector2(); }
|
|
}
|
|
|
|
public int BottomPos
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public int SeaFloorTopPos
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public LevelWall SeaFloor
|
|
{
|
|
get { return seaFloor; }
|
|
}
|
|
|
|
public List<Ruin> Ruins
|
|
{
|
|
get { return ruins; }
|
|
}
|
|
|
|
public List<LevelWall> ExtraWalls
|
|
{
|
|
get { return extraWalls; }
|
|
}
|
|
|
|
public List<List<Point>> SmallTunnels
|
|
{
|
|
get { return smallTunnels; }
|
|
}
|
|
|
|
public List<InterestingPosition> PositionsOfInterest
|
|
{
|
|
get { return positionsOfInterest; }
|
|
}
|
|
|
|
public Submarine StartOutpost { get; private set; }
|
|
public Submarine EndOutpost { get; private set; }
|
|
|
|
public string Seed
|
|
{
|
|
get { return seed; }
|
|
}
|
|
|
|
public Biome Biome;
|
|
|
|
/// <summary>
|
|
/// A random integer assigned at the end of 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 int EqualityCheckVal
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float Difficulty
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Body TopBarrier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Body BottomBarrier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public LevelObjectManager LevelObjectManager
|
|
{
|
|
get { return levelObjectManager; }
|
|
}
|
|
|
|
public bool Mirrored
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public LevelGenerationParams GenerationParams
|
|
{
|
|
get { return generationParams; }
|
|
}
|
|
|
|
public Color BackgroundTextureColor
|
|
{
|
|
get { return generationParams.BackgroundTextureColor; }
|
|
}
|
|
|
|
public Color BackgroundColor
|
|
{
|
|
get { return generationParams.BackgroundColor; }
|
|
}
|
|
|
|
public Color WallColor
|
|
{
|
|
get { return generationParams.WallColor; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates a level (the Generate-function still needs to be called before the level is playable)
|
|
/// </summary>
|
|
/// <param name="difficulty">A scalar between 0-100</param>
|
|
/// <param name="sizeFactor">A scalar between 0-1 (0 = the minimum width defined in the generation params is used, 1 = the max width is used)</param>
|
|
public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome)
|
|
: base(null)
|
|
{
|
|
|
|
this.seed = seed;
|
|
this.Biome = biome;
|
|
this.Difficulty = difficulty;
|
|
this.generationParams = generationParams;
|
|
|
|
sizeFactor = MathHelper.Clamp(sizeFactor, 0.0f, 1.0f);
|
|
int width = (int)MathHelper.Lerp(generationParams.MinWidth, generationParams.MaxWidth, sizeFactor);
|
|
|
|
borders = new Rectangle(0, 0,
|
|
(width / GridCellSize) * GridCellSize,
|
|
(generationParams.Height / GridCellSize) * GridCellSize);
|
|
|
|
//remove from entity dictionary
|
|
base.Remove();
|
|
}
|
|
|
|
public static Level CreateRandom(LocationConnection locationConnection)
|
|
{
|
|
string seed = locationConnection.Locations[0].Name + locationConnection.Locations[1].Name;
|
|
|
|
float sizeFactor = MathUtils.InverseLerp(
|
|
MapGenerationParams.Instance.SmallLevelConnectionLength,
|
|
MapGenerationParams.Instance.LargeLevelConnectionLength,
|
|
locationConnection.Length);
|
|
|
|
return new Level(seed,
|
|
locationConnection.Difficulty,
|
|
sizeFactor,
|
|
LevelGenerationParams.GetRandom(seed, locationConnection.Biome),
|
|
locationConnection.Biome);
|
|
}
|
|
|
|
public static Level CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null)
|
|
{
|
|
if (seed == "")
|
|
{
|
|
seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString();
|
|
}
|
|
|
|
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
|
|
|
if (generationParams == null) generationParams = LevelGenerationParams.GetRandom(seed);
|
|
var biome = LevelGenerationParams.GetBiomes().Find(b => generationParams.AllowedBiomes.Contains(b));
|
|
|
|
return new Level(
|
|
seed,
|
|
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server),
|
|
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server),
|
|
generationParams,
|
|
biome);
|
|
}
|
|
|
|
public void Generate(bool mirror)
|
|
{
|
|
if (loaded != null) loaded.Remove();
|
|
loaded = this;
|
|
|
|
levelObjectManager = new LevelObjectManager();
|
|
|
|
Mirrored = mirror;
|
|
|
|
#if CLIENT
|
|
if (backgroundCreatureManager == null)
|
|
{
|
|
var files = GameMain.Instance.GetFilesOfType(ContentType.BackgroundCreaturePrefabs);
|
|
if (files.Count() > 0)
|
|
backgroundCreatureManager = new BackgroundCreatureManager(files);
|
|
else
|
|
backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
|
|
}
|
|
#endif
|
|
Stopwatch sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
positionsOfInterest = new List<InterestingPosition>();
|
|
extraWalls = new List<LevelWall>();
|
|
bodies = new List<Body>();
|
|
List<Vector2> sites = new List<Vector2>();
|
|
|
|
Voronoi voronoi = new Voronoi(1.0);
|
|
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
|
|
|
#if CLIENT
|
|
renderer = new LevelRenderer(this);
|
|
#endif
|
|
|
|
SeaFloorTopPos = generationParams.SeaFloorDepth + generationParams.MountainHeightMax + generationParams.SeaFloorVariance;
|
|
|
|
int minWidth = 6500;
|
|
int maxWidth = 50000;
|
|
if (Submarine.MainSub != null)
|
|
{
|
|
Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders();
|
|
minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height));
|
|
minWidth = Math.Min(minWidth, maxWidth);
|
|
}
|
|
|
|
Rectangle pathBorders = borders;
|
|
pathBorders.Inflate(-minWidth * 2, -minWidth);
|
|
|
|
Debug.Assert(pathBorders.Width > 0 && pathBorders.Height > 0, "The size of the level's path area was negative.");
|
|
|
|
startPosition = new Point(
|
|
Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server),
|
|
Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server));
|
|
|
|
endPosition = new Point(
|
|
borders.Width - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server),
|
|
Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server));
|
|
|
|
//----------------------------------------------------------------------------------
|
|
//generate the initial nodes for the main path and smaller tunnels
|
|
//----------------------------------------------------------------------------------
|
|
|
|
List<Point> pathNodes = new List<Point>();
|
|
pathNodes.Add(new Point(startPosition.X, borders.Height));
|
|
|
|
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.Server))
|
|
{
|
|
pathNodes.Add(new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server)));
|
|
}
|
|
|
|
pathNodes.Add(new Point(endPosition.X, borders.Height));
|
|
|
|
if (pathNodes.Count <= 2)
|
|
{
|
|
pathNodes.Insert(1, borders.Center);
|
|
}
|
|
|
|
GenerateTunnels(pathNodes, minWidth);
|
|
|
|
//----------------------------------------------------------------------------------
|
|
//generate voronoi sites
|
|
//----------------------------------------------------------------------------------
|
|
|
|
Point siteInterval = generationParams.VoronoiSiteInterval;
|
|
int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y);
|
|
Point siteVariance = generationParams.VoronoiSiteVariance;
|
|
List<double> siteCoordsX = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
|
|
List<double> siteCoordsY = new List<double>((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y));
|
|
for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X)
|
|
{
|
|
for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y)
|
|
{
|
|
int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server);
|
|
int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server);
|
|
|
|
if (smallTunnels.Any(t => t.Any(node => MathUtils.DistanceSquared(node.X, node.Y, siteX, siteY) < siteIntervalSqr)))
|
|
{
|
|
//add some more sites around the small tunnels to generate more small voronoi cells
|
|
if (x < borders.Width - siteInterval.X)
|
|
{
|
|
siteCoordsX.Add(x + siteInterval.X / 2);
|
|
siteCoordsY.Add(y);
|
|
}
|
|
if (y < borders.Height - siteInterval.Y)
|
|
{
|
|
siteCoordsX.Add(x);
|
|
siteCoordsY.Add(y + siteInterval.Y / 2);
|
|
}
|
|
if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y)
|
|
{
|
|
siteCoordsX.Add(x + siteInterval.X / 2);
|
|
siteCoordsY.Add(y + siteInterval.Y / 2);
|
|
}
|
|
}
|
|
|
|
siteCoordsX.Add(siteX);
|
|
siteCoordsY.Add(siteY);
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// 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);
|
|
|
|
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, false);
|
|
|
|
for (int i = 2; i < mainPath.Count; i += 3)
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(
|
|
new Point((int)mainPath[i].Site.Coord.X, (int)mainPath[i].Site.Coord.Y),
|
|
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.ToVector2(),
|
|
SpawnType.Enemy,
|
|
submarine: null);
|
|
}
|
|
|
|
startPosition.X = (int)pathCells[0].Site.Coord.X;
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// tunnels through the tunnel nodes
|
|
//----------------------------------------------------------------------------------
|
|
|
|
List<List<Point>> validTunnels = new List<List<Point>>();
|
|
foreach (List<Point> 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;
|
|
|
|
int mainPathCellCount = 0;
|
|
for (int j = 0; j < tunnel.Count; j++)
|
|
{
|
|
int tunnelCellIndex = CaveGenerator.FindCellIndex(tunnel[j], cells, cellGrid, GridCellSize, 1);
|
|
if (tunnelCellIndex > -1 && cells[tunnelCellIndex].CellType == CellType.Path) mainPathCellCount++;
|
|
}
|
|
if (mainPathCellCount > tunnel.Count / 2) 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));
|
|
validTunnels.Add(tunnel);
|
|
pathCells.AddRange(newPathCells);
|
|
}
|
|
smallTunnels = validTunnels;
|
|
|
|
sw2.Restart();
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// remove unnecessary cells and create some holes at the bottom of the level
|
|
//----------------------------------------------------------------------------------
|
|
|
|
cells = CleanCells(pathCells);
|
|
|
|
int xPadding = borders.Width / 5;
|
|
int yPadding = borders.Height / 5;
|
|
pathCells.AddRange(CreateBottomHoles(generationParams.BottomHoleProbability, new Rectangle(
|
|
xPadding, 0,
|
|
borders.Width - xPadding * 2, borders.Height - yPadding)));
|
|
|
|
foreach (VoronoiCell cell in cells)
|
|
{
|
|
if (cell.Site.Coord.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();
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// 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 (!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 (!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 (List<Point> smallTunnel in smallTunnels)
|
|
{
|
|
for (int i = 0; i < smallTunnel.Count; i++)
|
|
{
|
|
smallTunnel[i] = new Point(borders.Width - smallTunnel[i].X, smallTunnel[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);
|
|
}
|
|
|
|
foreach (WayPoint waypoint in WayPoint.WayPointList)
|
|
{
|
|
if (waypoint.Submarine != null) continue;
|
|
waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f));
|
|
}
|
|
|
|
startPosition.X = borders.Width - startPosition.X;
|
|
endPosition.X = borders.Width - endPosition.X;
|
|
}
|
|
|
|
foreach (VoronoiCell cell in cells)
|
|
{
|
|
int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize);
|
|
int y = (int)Math.Floor(cell.Site.Coord.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, this, mirror);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// 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.Position.X < 5000 || pos.Position.X > Size.X - 5000) continue;
|
|
if (Math.Abs(pos.Position.X - StartPosition.X) < 10000) continue;
|
|
if (Math.Abs(pos.Position.Y - StartPosition.Y) < 10000) continue;
|
|
if (GetTooCloseCells(pos.Position.ToVector2(), minWidth * 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.Server)];
|
|
float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server);
|
|
var newChunk = new LevelWall(CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f), Color.White, this, true)
|
|
{
|
|
MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server),
|
|
MoveAmount = new Vector2(0.0f, minWidth * 0.7f)
|
|
};
|
|
newChunk.Body.Position = ConvertUnits.ToSimUnits(selectedPos.ToVector2());
|
|
newChunk.Body.BodyType = BodyType.Dynamic;
|
|
newChunk.Body.FixedRotation = true;
|
|
newChunk.Body.LinearDamping = 0.5f;
|
|
newChunk.Body.GravityScale = 0.0f;
|
|
newChunk.Body.Mass *= 10.0f;
|
|
extraWalls.Add(newChunk);
|
|
iceChunkPositions.Remove(selectedPos);
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// generate the bodies and rendered triangles of the cells
|
|
//----------------------------------------------------------------------------------
|
|
|
|
startPosition.Y = borders.Height;
|
|
endPosition.Y = borders.Height;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List<Vector2[]> triangles));
|
|
|
|
#if CLIENT
|
|
renderer.SetBodyVertices(CaveGenerator.GenerateRenderVerticeList(triangles).ToArray(), generationParams.WallColor);
|
|
renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cellsWithBody, this), generationParams.WallColor);
|
|
#endif
|
|
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// create (placeholder) outposts at the start and end of the level
|
|
//----------------------------------------------------------------------------------
|
|
|
|
CreateOutposts();
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// top barrier & sea floor
|
|
//----------------------------------------------------------------------------------
|
|
|
|
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(mirror);
|
|
|
|
levelObjectManager.PlaceObjects(this, generationParams.LevelObjectAmount);
|
|
|
|
GenerateItems();
|
|
|
|
EqualityCheckVal = Rand.Int(int.MaxValue, Rand.RandSync.Server);
|
|
|
|
#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(MapEntity.mapEntityList.FindAll(me => me.Submarine == null), false);
|
|
|
|
Debug.WriteLine("Generatelevel: " + sw2.ElapsedMilliseconds + " ms");
|
|
sw2.Restart();
|
|
|
|
if (mirror)
|
|
{
|
|
Point temp = startPosition;
|
|
startPosition = endPosition;
|
|
endPosition = temp;
|
|
}
|
|
if (StartOutpost != null)
|
|
{
|
|
startPosition = new Point((int)StartOutpost.WorldPosition.X, (int)StartOutpost.WorldPosition.Y);
|
|
}
|
|
if (EndOutpost != null)
|
|
{
|
|
endPosition = new Point((int)EndOutpost.WorldPosition.X, (int)EndOutpost.WorldPosition.Y);
|
|
}
|
|
|
|
Debug.WriteLine("**********************************************************************************");
|
|
Debug.WriteLine("Generated a map with " + siteCoordsX.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);
|
|
}
|
|
|
|
//assign an ID to make entity events work
|
|
ID = FindFreeID();
|
|
}
|
|
|
|
|
|
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.Site.Coord.X, cell.Site.Coord.Y)) 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].Site.Coord.X, borders.Height, 10, 10), null);
|
|
wayPoints.Add(newWaypoint);
|
|
|
|
for (int i = 0; i < pathCells.Count; i++)
|
|
{
|
|
pathCells[i].CellType = CellType.Path;
|
|
|
|
newWaypoint = new WayPoint(new Rectangle((int)pathCells[i].Site.Coord.X, (int)pathCells[i].Center.Y, 10, 10), null);
|
|
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].Site.Coord.X, borders.Height, 10, 10), null);
|
|
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);
|
|
|
|
float minDistSqr = minDistance * minDistance;
|
|
foreach (VoronoiCell cell in closeCells)
|
|
{
|
|
bool tooClose = false;
|
|
foreach (GraphEdge edge in cell.Edges)
|
|
{
|
|
if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr ||
|
|
Vector2.DistanceSquared(edge.Point2, position) < minDistSqr)
|
|
{
|
|
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(bool mirror)
|
|
{
|
|
BottomPos = generationParams.SeaFloorDepth;
|
|
SeaFloorTopPos = BottomPos;
|
|
|
|
bottomPositions = new List<Point>
|
|
{
|
|
new Point(0, BottomPos)
|
|
};
|
|
|
|
int mountainCount = Rand.Range(generationParams.MountainCountMin, generationParams.MountainCountMax, Rand.RandSync.Server);
|
|
for (int i = 0; i < mountainCount; i++)
|
|
{
|
|
bottomPositions.Add(
|
|
new Point(Size.X / (mountainCount + 1) * (i + 1),
|
|
BottomPos + Rand.Range(generationParams.MountainHeightMin, generationParams.MountainHeightMax, Rand.RandSync.Server)));
|
|
}
|
|
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, Rand.RandSync.Server)));
|
|
i++;
|
|
}
|
|
|
|
currInverval /= 2;
|
|
}
|
|
|
|
if (mirror)
|
|
{
|
|
for (int i = 0; i < bottomPositions.Count; i++)
|
|
{
|
|
bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y);
|
|
}
|
|
}
|
|
|
|
SeaFloorTopPos = bottomPositions.Max(p => p.Y);
|
|
seaFloor = new LevelWall(bottomPositions.Select(p => p.ToVector2()).ToList(), new Vector2(0.0f, -2000.0f), generationParams.WallColor, this);
|
|
extraWalls.Add(seaFloor);
|
|
|
|
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<Point> pathNodes, int pathWidth)
|
|
{
|
|
smallTunnels = new List<List<Point>>();
|
|
for (int i = 0; i < generationParams.SmallTunnelCount; i++)
|
|
{
|
|
var tunnelStartPos = pathNodes[Rand.Range(1, pathNodes.Count - 2, Rand.RandSync.Server)];
|
|
|
|
int tunnelLength = Rand.Range(
|
|
generationParams.SmallTunnelLengthRange.X,
|
|
generationParams.SmallTunnelLengthRange.Y,
|
|
Rand.RandSync.Server);
|
|
|
|
List<Point> tunnelNodes = new List<Point>()
|
|
{
|
|
tunnelStartPos,
|
|
tunnelStartPos + new Point(0, Math.Sign(tunnelStartPos.Y - Size.Y / 2) * pathWidth * 2)
|
|
};
|
|
|
|
List<Point> 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<Point> branch = GenerateTunnel(
|
|
new List<Point>() { 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<Point> GenerateTunnel(List<Point> tunnelNodes, float tunnelLength, List<Point> avoidNodes)
|
|
{
|
|
int sectionLength = 1000;
|
|
|
|
float currLength = 0.0f;
|
|
DoubleVector2 dir = null;
|
|
while (currLength < tunnelLength)
|
|
{
|
|
var prevDir = dir;
|
|
dir = Rand.Vector(1.0, Rand.RandSync.Server);
|
|
|
|
dir.Y += Math.Sign(tunnelNodes[tunnelNodes.Count - 1].Y - Size.Y / 2) * 0.5f;
|
|
if (prevDir != null)
|
|
{
|
|
dir.X = (dir.X + prevDir.X) / 2.0;
|
|
dir.Y = (dir.Y + prevDir.Y) / 2.0;
|
|
}
|
|
|
|
double avoidDist = 20000;
|
|
double avoidDistSqr = avoidDist * avoidDist;
|
|
foreach (Point pathNode in avoidNodes)
|
|
{
|
|
double diffX = tunnelNodes[tunnelNodes.Count - 1].X - pathNode.X;
|
|
double diffY = tunnelNodes[tunnelNodes.Count - 1].Y - pathNode.Y;
|
|
if (Math.Abs(diffX) < 1.0f || Math.Abs(diffY) < 1.0f) continue;
|
|
|
|
double distSqr = (diffX * diffX + diffY * diffY);
|
|
Debug.Assert(distSqr > 0);
|
|
if (distSqr < avoidDistSqr)
|
|
{
|
|
double dist = Math.Sqrt(distSqr);
|
|
|
|
dir.X += (diffX / dist) * (1.0f - dist / avoidDist);
|
|
dir.Y += (diffY / dist) * (1.0f - dist / avoidDist);
|
|
}
|
|
}
|
|
|
|
dir.Normalize();
|
|
|
|
if (tunnelNodes.Last().Y + dir.Y > Size.Y)
|
|
{
|
|
//head back down if the tunnel has reached the top of the level
|
|
dir.Y = -dir.Y;
|
|
}
|
|
else if (tunnelNodes.Last().Y + dir.Y * 500 < 500)
|
|
{
|
|
//head back up if reached the bottom of the level
|
|
dir.Y = -dir.Y;
|
|
}
|
|
else if (tunnelNodes.Last().Y + dir.Y + dir.Y < 0.0f ||
|
|
tunnelNodes.Last().Y + dir.Y + dir.Y < SeaFloorTopPos)
|
|
{
|
|
//head back up if reached the sea floor
|
|
dir.Y = -dir.Y;
|
|
}
|
|
|
|
Point nextNode = tunnelNodes.Last() + new Point((int)(dir.X * sectionLength), (int)(dir.Y * sectionLength));
|
|
nextNode.X = MathHelper.Clamp(nextNode.X, 500, Size.X - 500);
|
|
nextNode.Y = MathHelper.Clamp(nextNode.Y, SeaFloorTopPos, Size.Y - 500);
|
|
tunnelNodes.Add(nextNode);
|
|
currLength += sectionLength;
|
|
}
|
|
|
|
return tunnelNodes;
|
|
}
|
|
|
|
private void GenerateRuin(List<VoronoiCell> mainPath, Level level, bool mirror)
|
|
{
|
|
var ruinGenerationParams = RuinGenerationParams.GetRandom();
|
|
|
|
Point ruinSize = new Point(
|
|
Rand.Range(ruinGenerationParams.SizeMin.X, ruinGenerationParams.SizeMax.X, Rand.RandSync.Server),
|
|
Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server));
|
|
int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2;
|
|
|
|
int cellIndex = Rand.Int(cells.Count, Rand.RandSync.Server);
|
|
Point ruinPos = new Point((int)cells[cellIndex].Site.Coord.X, (int)cells[cellIndex].Site.Coord.X);
|
|
|
|
//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);
|
|
|
|
double minDist = ruinRadius * 2;
|
|
double minDistSqr = minDist * minDist;
|
|
|
|
int iter = 0;
|
|
while (mainPath.Any(p => MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, p.Site.Coord.X, p.Site.Coord.Y) < minDistSqr) ||
|
|
ruins.Any(r => r.Area.Intersects(new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize))))
|
|
{
|
|
double weighedPathPosX = ruinPos.X;
|
|
double weighedPathPosY = ruinPos.Y;
|
|
iter++;
|
|
|
|
foreach (VoronoiCell pathCell in mainPath)
|
|
{
|
|
double diffX = ruinPos.X - pathCell.Site.Coord.X;
|
|
double diffY = ruinPos.Y - pathCell.Site.Coord.Y;
|
|
|
|
double distSqr = diffX * diffX + diffY * diffY;
|
|
if (distSqr < 1.0)
|
|
{
|
|
diffX = 0;
|
|
diffY = 1;
|
|
distSqr = 1.0;
|
|
}
|
|
if (distSqr > 10000.0 * 10000.0) continue;
|
|
|
|
double dist = Math.Sqrt(distSqr);
|
|
double moveAmountX = 100.0 * diffX / dist;
|
|
double moveAmountY = 100.0 * diffY / dist;
|
|
|
|
weighedPathPosX += moveAmountX;
|
|
weighedPathPosY += moveAmountY;
|
|
weighedPathPosY = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPosY);
|
|
}
|
|
|
|
Rectangle ruinArea = new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize);
|
|
foreach (Ruin otherRuin in ruins)
|
|
{
|
|
if (!otherRuin.Area.Intersects(ruinArea)) continue;
|
|
|
|
double diffX = ruinArea.Center.X - otherRuin.Area.Center.X;
|
|
double diffY = ruinArea.Center.Y - otherRuin.Area.Center.Y;
|
|
|
|
double distSqr = diffX * diffX + diffY * diffY;
|
|
if (distSqr < 0.01f)
|
|
{
|
|
diffX = 0;
|
|
diffY = -1;
|
|
distSqr = 1;
|
|
}
|
|
|
|
double dist = Math.Sqrt(distSqr);
|
|
double moveAmountX = diffX / dist;
|
|
double moveAmountY = diffY / dist;
|
|
|
|
int move = (Math.Max(ruinArea.Width, ruinArea.Height) + Math.Max(otherRuin.Area.Width, otherRuin.Area.Height)) / 2;
|
|
moveAmountX *= move;
|
|
moveAmountY *= move;
|
|
|
|
weighedPathPosX += moveAmountX;
|
|
weighedPathPosY += moveAmountY;
|
|
}
|
|
ruinPos = new Point((int)weighedPathPosX, (int)weighedPathPosY);
|
|
|
|
//if we can't find a suitable position after 10 000 iterations, give up
|
|
if (iter > 10000)
|
|
{
|
|
if (ruins.Count > 0)
|
|
{
|
|
//we already have some ruins, don't add this one at all
|
|
return;
|
|
}
|
|
string errorMsg = "Failed to find a suitable position for ruins. Level seed: " + seed +
|
|
", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Name);
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Level.GenerateRuins:PosNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
break;
|
|
}
|
|
//if we haven't found a position after 500 iterations, try another starting point
|
|
else if (iter > 500 && iter % 500 == 0)
|
|
{
|
|
int newCellIndex = Rand.Int(cells.Count, Rand.RandSync.Server);
|
|
ruinPos = new Point((int)cells[newCellIndex].Site.Coord.X, (int)cells[newCellIndex].Site.Coord.X);
|
|
}
|
|
ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2);
|
|
ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2);
|
|
}
|
|
|
|
if (Math.Abs(ruinPos.X) > int.MaxValue / 2 || Math.Abs(ruinPos.Y) > int.MaxValue / 2)
|
|
{
|
|
DebugConsole.ThrowError("Something went wrong during ruin generation. Ruin position: " + ruinPos);
|
|
return;
|
|
}
|
|
|
|
VoronoiCell closestPathCell = null;
|
|
double closestDist = 0.0f;
|
|
foreach (VoronoiCell pathCell in mainPath)
|
|
{
|
|
double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y);
|
|
if (closestPathCell == null || dist < closestDist)
|
|
{
|
|
closestPathCell = pathCell;
|
|
closestDist = dist;
|
|
}
|
|
}
|
|
|
|
var ruin = new Ruin(closestPathCell, cells, ruinGenerationParams, new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize), mirror);
|
|
ruins.Add(ruin);
|
|
|
|
ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance));
|
|
int waypointCount = 0;
|
|
foreach (WayPoint wp in WayPoint.WayPointList)
|
|
{
|
|
if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; }
|
|
if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition)))
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin));
|
|
waypointCount++;
|
|
}
|
|
}
|
|
|
|
//not enough waypoints inside ruins -> create some spawn positions manually
|
|
for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++)
|
|
{
|
|
positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, 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, out _))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//cast a ray from the closest path cell towards the ruin and remove the cell it hits
|
|
//to ensure that there's always at least one way from the main tunnel to the ruin
|
|
List<VoronoiCell> validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed);
|
|
foreach (VoronoiCell cell in validCells)
|
|
{
|
|
foreach (GraphEdge e in cell.Edges)
|
|
{
|
|
if (MathUtils.LinesIntersect(closestPathCell.Center, ruinPos.ToVector2(), e.Point1, e.Point2))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
if (cell.CellType == CellType.Removed)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateItems()
|
|
{
|
|
string levelName = generationParams.Name.ToLowerInvariant();
|
|
List<Pair<ItemPrefab, float>> levelItems = new List<Pair<ItemPrefab, float>>();
|
|
foreach (MapEntityPrefab mapEntityPrefab in MapEntityPrefab.List)
|
|
{
|
|
if (!(mapEntityPrefab is ItemPrefab itemPrefab)) { continue; }
|
|
|
|
if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) ||
|
|
itemPrefab.LevelCommonness.TryGetValue("", out commonness))
|
|
{
|
|
levelItems.Add(new Pair<ItemPrefab, float>(itemPrefab, commonness));
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < generationParams.ItemCount; i++)
|
|
{
|
|
var selectedPrefab = ToolBox.SelectWeightedRandom(
|
|
levelItems.Select(it => it.First).ToList(),
|
|
levelItems.Select(it => it.Second).ToList(),
|
|
Rand.RandSync.Server);
|
|
if (selectedPrefab == null) { break; }
|
|
|
|
var selectedCell = cells[Rand.Int(cells.Count, Rand.RandSync.Server)];
|
|
var selectedEdge = selectedCell.Edges.GetRandom(e => e.IsSolid && !e.OutsideLevel, Rand.RandSync.Server);
|
|
if (selectedEdge == null) continue;
|
|
|
|
float edgePos = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
|
|
Vector2 selectedPos = Vector2.Lerp(selectedEdge.Point1, selectedEdge.Point2, edgePos);
|
|
Vector2 edgeNormal = selectedEdge.GetNormal(selectedCell);
|
|
|
|
var item = new Item(selectedPrefab, selectedPos, submarine: null);
|
|
item.Move(edgeNormal * item.Rect.Height / 2);
|
|
|
|
var holdable = item.GetComponent<Holdable>();
|
|
if (holdable == null)
|
|
{
|
|
DebugConsole.ThrowError("Error while placing items in the level - item \"" + item.Name + "\" is not holdable and cannot be attached to the level walls.");
|
|
}
|
|
else
|
|
{
|
|
holdable.AttachToWall();
|
|
#if CLIENT
|
|
item.SpriteRotation = -MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2;
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f)
|
|
{
|
|
if (!positionsOfInterest.Any())
|
|
{
|
|
return new Vector2(Size.X / 2, Size.Y / 2);
|
|
}
|
|
|
|
Vector2 position = Vector2.Zero;
|
|
|
|
offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall);
|
|
|
|
int tries = 0;
|
|
do
|
|
{
|
|
Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 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 | Physics.CollisionWall) != 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)
|
|
{
|
|
bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos);
|
|
position = pos.ToVector2();
|
|
return success;
|
|
}
|
|
|
|
public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position)
|
|
{
|
|
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 (!suitablePositions.Any())
|
|
{
|
|
string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace;
|
|
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
#if DEBUG
|
|
DebugConsole.ThrowError(errorMsg);
|
|
#endif
|
|
position = positionsOfInterest[Rand.Int(positionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : 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.IsOutpost) { continue; }
|
|
farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs);
|
|
}
|
|
}
|
|
if (!farEnoughPositions.Any())
|
|
{
|
|
string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace;
|
|
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsSDK.Net.EGAErrorSeverity.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.Server : Rand.RandSync.Unsynced))].Position;
|
|
return true;
|
|
}
|
|
|
|
public void Update(float deltaTime, Camera cam)
|
|
{
|
|
levelObjectManager.Update(deltaTime);
|
|
|
|
foreach (LevelWall wall in ExtraWalls)
|
|
{
|
|
wall.Update(deltaTime);
|
|
}
|
|
|
|
if (GameMain.Server != null)
|
|
{
|
|
networkUpdateTimer += deltaTime;
|
|
if (networkUpdateTimer > NetworkUpdateInterval)
|
|
{
|
|
if (extraWalls.Any(w => w.Body.BodyType != BodyType.Static))
|
|
{
|
|
GameMain.Server.CreateEntityEvent(this);
|
|
}
|
|
networkUpdateTimer = 0.0f;
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
backgroundCreatureManager.Update(deltaTime, cam);
|
|
WaterRenderer.Instance?.ScrollWater(Vector2.UnitY, (float)deltaTime);
|
|
renderer.Update(deltaTime, cam);
|
|
#endif
|
|
}
|
|
|
|
public Vector2 GetBottomPosition(float xPosition)
|
|
{
|
|
int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1));
|
|
if (index < 0 || index >= bottomPositions.Count - 1) return new Vector2(xPosition, BottomPos);
|
|
|
|
float yPos = MathHelper.Lerp(
|
|
bottomPositions[index].Y,
|
|
bottomPositions[index + 1].Y,
|
|
(xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X));
|
|
|
|
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;
|
|
}
|
|
|
|
public List<VoronoiCell> GetCells(Vector2 worldPos, int searchDepth = 2)
|
|
{
|
|
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);
|
|
|
|
List<VoronoiCell> cells = new List<VoronoiCell>();
|
|
for (int y = startY; y <= endY; y++)
|
|
{
|
|
for (int x = startX; x <= endX; x++)
|
|
{
|
|
cells.AddRange(cellGrid[x, y]);
|
|
}
|
|
}
|
|
|
|
foreach (LevelWall wall in extraWalls)
|
|
{
|
|
foreach (VoronoiCell cell in wall.Cells)
|
|
{
|
|
cells.Add(cell);
|
|
}
|
|
}
|
|
|
|
return cells;
|
|
}
|
|
|
|
private void CreateOutposts()
|
|
{
|
|
var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Outpost);
|
|
if (outpostFiles.Count() == 0)
|
|
{
|
|
DebugConsole.ThrowError("No outpost files found in the selected content packages");
|
|
return;
|
|
}
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
//no outposts at either side of the level when there's more than one main sub (combat missions)
|
|
if (Submarine.MainSubs.Length > 1 && Submarine.MainSubs[0] != null && Submarine.MainSubs[1] != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//only create a starting outpost in campaign mode
|
|
if (GameMain.GameSession?.GameMode as CampaignMode == null && ((i == 0) == !Mirrored))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server);
|
|
var outpost = new Submarine(outpostFile, tryLoad: false);
|
|
outpost.Load(unloadPrevious: false);
|
|
outpost.MakeOutpost();
|
|
|
|
Point? minSize = null;
|
|
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);
|
|
}
|
|
|
|
outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize));
|
|
if ((i == 0) == !Mirrored)
|
|
{
|
|
StartOutpost = outpost;
|
|
}
|
|
else
|
|
{
|
|
EndOutpost = outpost;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Remove()
|
|
{
|
|
base.Remove();
|
|
#if CLIENT
|
|
if (renderer != null)
|
|
{
|
|
renderer.Dispose();
|
|
renderer = null;
|
|
}
|
|
#endif
|
|
|
|
if (levelObjectManager != null)
|
|
{
|
|
levelObjectManager.Remove();
|
|
levelObjectManager = null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
|
|
{
|
|
foreach (LevelWall levelWall in extraWalls)
|
|
{
|
|
if (levelWall.Body.BodyType == BodyType.Static) continue;
|
|
|
|
msg.Write(levelWall.Body.Position.X);
|
|
msg.Write(levelWall.Body.Position.Y);
|
|
msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16);
|
|
}
|
|
}
|
|
}
|
|
}
|