VGRoid support (#27659)
* Dungeon spawn support for grid spawns * Recursive dungeons working * Mask approach working * zack * More work * Fix recursive dungeons * Heap of work * weh * the cud * rar * Job * weh * weh * weh * Master merges * orch * weh * vgroid most of the work * Tweaks * Tweaks * weh * do do do do do do * Basic layout * Ore spawning working * Big breaking changes * Mob gen working * weh * Finalising * emo * More finalising * reverty * Reduce distance
This commit is contained in:
123
Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
Normal file
123
Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Handle BFS searches from Start->End. Doesn't consider NPC pathfinding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pathfinding args for a 1-many path.
|
||||||
|
/// </summary>
|
||||||
|
public record struct BreadthPathArgs()
|
||||||
|
{
|
||||||
|
public Vector2i Start;
|
||||||
|
public List<Vector2i> Ends;
|
||||||
|
|
||||||
|
public bool Diagonals = false;
|
||||||
|
|
||||||
|
public Func<Vector2i, float>? TileCost;
|
||||||
|
|
||||||
|
public int Limit = 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
|
||||||
|
/// </summary>
|
||||||
|
public SimplePathResult GetBreadthPath(BreadthPathArgs args)
|
||||||
|
{
|
||||||
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
||||||
|
var costSoFar = new Dictionary<Vector2i, float>();
|
||||||
|
var frontier = new PriorityQueue<Vector2i, float>();
|
||||||
|
|
||||||
|
costSoFar[args.Start] = 0f;
|
||||||
|
frontier.Enqueue(args.Start, 0f);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (args.Ends.Contains(node))
|
||||||
|
{
|
||||||
|
// Found target
|
||||||
|
var path = ReconstructPath(node, cameFrom);
|
||||||
|
|
||||||
|
return new SimplePathResult()
|
||||||
|
{
|
||||||
|
CameFrom = cameFrom,
|
||||||
|
Path = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var gCost = costSoFar[node];
|
||||||
|
|
||||||
|
if (args.Diagonals)
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
var neighbor = node + new Vector2i(x, y);
|
||||||
|
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
|
||||||
|
|
||||||
|
if (neighborCost.Equals(0f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f = g + h
|
||||||
|
// gScore is distance to the start node
|
||||||
|
// hScore is distance to the end node
|
||||||
|
var gScore = gCost + neighborCost;
|
||||||
|
|
||||||
|
// Slower to get here so just ignore it.
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
// pFactor is tie-breaker where the fscore is otherwise equal.
|
||||||
|
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
|
||||||
|
// There's other ways to do it but future consideration
|
||||||
|
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
||||||
|
// (i.e. somewhere between 1 and infinite)
|
||||||
|
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
||||||
|
frontier.Enqueue(neighbor, gScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = node + new Vector2i(x, y);
|
||||||
|
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
|
||||||
|
|
||||||
|
if (neighborCost.Equals(0f))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gScore = gCost + neighborCost;
|
||||||
|
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
|
||||||
|
frontier.Enqueue(neighbor, gScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SimplePathResult.NoPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs
Normal file
74
Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
|
||||||
|
{
|
||||||
|
// https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566
|
||||||
|
// declare all locals at the top so it's obvious how big the footprint is
|
||||||
|
int dx, dy, xinc, yinc, side, i, error;
|
||||||
|
|
||||||
|
// starting cell is always returned
|
||||||
|
if (!callback(start))
|
||||||
|
return;
|
||||||
|
|
||||||
|
xinc = (end.X < start.X) ? -1 : 1;
|
||||||
|
yinc = (end.Y < start.Y) ? -1 : 1;
|
||||||
|
dx = xinc * (end.X - start.X);
|
||||||
|
dy = yinc * (end.Y - start.Y);
|
||||||
|
var ax = start.X;
|
||||||
|
var ay = start.Y;
|
||||||
|
|
||||||
|
if (dx == dy) // Handle perfect diagonals
|
||||||
|
{
|
||||||
|
// I include this "optimization" for more aesthetic reasons, actually.
|
||||||
|
// While Bresenham's Line can handle perfect diagonals just fine, it adds
|
||||||
|
// additional cells to the line that make it not a perfect diagonal
|
||||||
|
// anymore. So, while this branch is ~twice as fast as the next branch,
|
||||||
|
// the real reason it is here is for style.
|
||||||
|
|
||||||
|
// Also, there *is* the reason of performance. If used for cell-based
|
||||||
|
// raycasts, for example, then perfect diagonals will check half as many
|
||||||
|
// cells.
|
||||||
|
|
||||||
|
while (dx --> 0)
|
||||||
|
{
|
||||||
|
ax += xinc;
|
||||||
|
ay += yinc;
|
||||||
|
if (!callback(new Vector2i(ax, ay)))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all other lines
|
||||||
|
|
||||||
|
side = -1 * ((dx == 0 ? yinc : xinc) - 1);
|
||||||
|
|
||||||
|
i = dx + dy;
|
||||||
|
error = dx - dy;
|
||||||
|
|
||||||
|
dx *= 2;
|
||||||
|
dy *= 2;
|
||||||
|
|
||||||
|
while (i --> 0)
|
||||||
|
{
|
||||||
|
if (error > 0 || error == side)
|
||||||
|
{
|
||||||
|
ax += xinc;
|
||||||
|
error -= dy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ay += yinc;
|
||||||
|
error += dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback(new Vector2i(ax, ay)))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate bool Vector2iCallback(Vector2i index);
|
||||||
|
}
|
||||||
154
Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs
Normal file
154
Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pathfinding args for a 1-1 path.
|
||||||
|
/// </summary>
|
||||||
|
public record struct SimplePathArgs()
|
||||||
|
{
|
||||||
|
public Vector2i Start;
|
||||||
|
public Vector2i End;
|
||||||
|
|
||||||
|
public bool Diagonals = false;
|
||||||
|
|
||||||
|
public int Limit = 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom tile-costs if applicable.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Vector2i, float>? TileCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct SimplePathResult
|
||||||
|
{
|
||||||
|
public static SimplePathResult NoPath = new();
|
||||||
|
|
||||||
|
public List<Vector2i> Path;
|
||||||
|
public Dictionary<Vector2i, Vector2i> CameFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets simple A* path from start to end. Can also supply an optional tile-cost for tiles.
|
||||||
|
/// </summary>
|
||||||
|
public SimplePathResult GetPath(SimplePathArgs args)
|
||||||
|
{
|
||||||
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
||||||
|
var costSoFar = new Dictionary<Vector2i, float>();
|
||||||
|
var frontier = new PriorityQueue<Vector2i, float>();
|
||||||
|
|
||||||
|
costSoFar[args.Start] = 0f;
|
||||||
|
frontier.Enqueue(args.Start, 0f);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (node == args.End)
|
||||||
|
{
|
||||||
|
// Found target
|
||||||
|
var path = ReconstructPath(args.End, cameFrom);
|
||||||
|
|
||||||
|
return new SimplePathResult()
|
||||||
|
{
|
||||||
|
CameFrom = cameFrom,
|
||||||
|
Path = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var gCost = costSoFar[node];
|
||||||
|
|
||||||
|
if (args.Diagonals)
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
var neighbor = node + new Vector2i(x, y);
|
||||||
|
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
|
||||||
|
|
||||||
|
if (neighborCost.Equals(0f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f = g + h
|
||||||
|
// gScore is distance to the start node
|
||||||
|
// hScore is distance to the end node
|
||||||
|
var gScore = gCost + neighborCost;
|
||||||
|
|
||||||
|
// Slower to get here so just ignore it.
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
// pFactor is tie-breaker where the fscore is otherwise equal.
|
||||||
|
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
|
||||||
|
// There's other ways to do it but future consideration
|
||||||
|
// The closer the fScore is to the actual distance then the better the pathfinder will be
|
||||||
|
// (i.e. somewhere between 1 and infinite)
|
||||||
|
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
|
||||||
|
var hScore = OctileDistance(args.End, neighbor) * (1.0f + 1.0f / 1000.0f);
|
||||||
|
var fScore = gScore + hScore;
|
||||||
|
frontier.Enqueue(neighbor, fScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = node + new Vector2i(x, y);
|
||||||
|
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
|
||||||
|
|
||||||
|
if (neighborCost.Equals(0f))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gScore = gCost + neighborCost;
|
||||||
|
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
|
||||||
|
// Still use octile even for manhattan distance.
|
||||||
|
var hScore = OctileDistance(args.End, neighbor) * 1.001f;
|
||||||
|
var fScore = gScore + hScore;
|
||||||
|
frontier.Enqueue(neighbor, fScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SimplePathResult.NoPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Vector2i> ReconstructPath(Vector2i end, Dictionary<Vector2i, Vector2i> cameFrom)
|
||||||
|
{
|
||||||
|
var path = new List<Vector2i>()
|
||||||
|
{
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
var node = end;
|
||||||
|
|
||||||
|
while (cameFrom.TryGetValue(node, out var source))
|
||||||
|
{
|
||||||
|
path.Add(source);
|
||||||
|
node = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.Reverse();
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
Normal file
180
Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
public record struct SimplifyPathArgs
|
||||||
|
{
|
||||||
|
public Vector2i Start;
|
||||||
|
public Vector2i End;
|
||||||
|
public List<Vector2i> Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct SplinePathResult()
|
||||||
|
{
|
||||||
|
public static SplinePathResult NoPath = new();
|
||||||
|
|
||||||
|
public List<Vector2i> Points = new();
|
||||||
|
|
||||||
|
public List<Vector2i> Path = new();
|
||||||
|
public Dictionary<Vector2i, Vector2i> CameFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct SplinePathArgs(SimplePathArgs Args)
|
||||||
|
{
|
||||||
|
public SimplePathArgs Args = Args;
|
||||||
|
|
||||||
|
public float MaxRatio = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum distance between subdivisions.
|
||||||
|
/// </summary>
|
||||||
|
public int Distance = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a spline path from start to end.
|
||||||
|
/// </summary>
|
||||||
|
public SplinePathResult GetSplinePath(SplinePathArgs args, Random random)
|
||||||
|
{
|
||||||
|
var start = args.Args.Start;
|
||||||
|
var end = args.Args.End;
|
||||||
|
|
||||||
|
var path = new List<Vector2i>();
|
||||||
|
|
||||||
|
var pairs = new ValueList<(Vector2i Start, Vector2i End)> { (start, end) };
|
||||||
|
var subdivided = true;
|
||||||
|
|
||||||
|
// Sub-divide recursively
|
||||||
|
while (subdivided)
|
||||||
|
{
|
||||||
|
// Sometimes we might inadvertantly get 2 nodes too close together so better to just check each one as it comes up instead.
|
||||||
|
var i = 0;
|
||||||
|
subdivided = false;
|
||||||
|
|
||||||
|
while (i < pairs.Count)
|
||||||
|
{
|
||||||
|
var pointA = pairs[i].Start;
|
||||||
|
var pointB = pairs[i].End;
|
||||||
|
var vector = pointB - pointA;
|
||||||
|
|
||||||
|
var halfway = vector / 2f;
|
||||||
|
|
||||||
|
// Finding the point
|
||||||
|
var adj = halfway.Length();
|
||||||
|
|
||||||
|
// Should we even subdivide.
|
||||||
|
if (adj <= args.Distance)
|
||||||
|
{
|
||||||
|
// Just check the next entry no double skip.
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
subdivided = true;
|
||||||
|
var opposite = args.MaxRatio * adj;
|
||||||
|
var hypotenuse = MathF.Sqrt(MathF.Pow(adj, 2) + MathF.Pow(opposite, 2));
|
||||||
|
|
||||||
|
// Okay so essentially we have 2 points and no poly
|
||||||
|
// We add 2 other points to form a diamond and want some point halfway between randomly offset.
|
||||||
|
var angle = new Angle(MathF.Atan(opposite / adj));
|
||||||
|
var pointAPerp = pointA + angle.RotateVec(halfway).Normalized() * hypotenuse;
|
||||||
|
var pointBPerp = pointA + (-angle).RotateVec(halfway).Normalized() * hypotenuse;
|
||||||
|
|
||||||
|
var perpLine = pointBPerp - pointAPerp;
|
||||||
|
var perpHalfway = perpLine.Length() / 2f;
|
||||||
|
|
||||||
|
var splinePoint = (pointAPerp + perpLine.Normalized() * random.NextFloat(-args.MaxRatio, args.MaxRatio) * perpHalfway).Floored();
|
||||||
|
|
||||||
|
// We essentially take (A, B) and turn it into (A, C) & (C, B)
|
||||||
|
pairs[i] = (pointA, splinePoint);
|
||||||
|
pairs.Insert(i + 1, (splinePoint, pointB));
|
||||||
|
|
||||||
|
i+= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var spline = new ValueList<Vector2i>(pairs.Count - 1)
|
||||||
|
{
|
||||||
|
start
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
spline.Add(pair.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to pathfind between each node on the spline.
|
||||||
|
|
||||||
|
// TODO: Add rotation version or straight-line version for pathfinder config
|
||||||
|
// Move the worm pathfinder to here I think.
|
||||||
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
||||||
|
|
||||||
|
// TODO: Need to get rid of the branch bullshit.
|
||||||
|
var points = new List<Vector2i>();
|
||||||
|
|
||||||
|
for (var i = 0; i < spline.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var point = spline[i];
|
||||||
|
var target = spline[i + 1];
|
||||||
|
points.Add(point);
|
||||||
|
var aStarArgs = args.Args with { Start = point, End = target };
|
||||||
|
|
||||||
|
var aStarResult = GetPath(aStarArgs);
|
||||||
|
|
||||||
|
if (aStarResult == SimplePathResult.NoPath)
|
||||||
|
return SplinePathResult.NoPath;
|
||||||
|
|
||||||
|
path.AddRange(aStarResult.Path[0..]);
|
||||||
|
|
||||||
|
foreach (var a in aStarResult.CameFrom)
|
||||||
|
{
|
||||||
|
cameFrom[a.Key] = a.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
points.Add(spline[^1]);
|
||||||
|
|
||||||
|
var simple = SimplifyPath(new SimplifyPathArgs()
|
||||||
|
{
|
||||||
|
Start = args.Args.Start,
|
||||||
|
End = args.Args.End,
|
||||||
|
Path = path,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new SplinePathResult()
|
||||||
|
{
|
||||||
|
Path = simple,
|
||||||
|
CameFrom = cameFrom,
|
||||||
|
Points = points,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does a simpler pathfinder over the nodes to prune unnecessary branches.
|
||||||
|
/// </summary>
|
||||||
|
public List<Vector2i> SimplifyPath(SimplifyPathArgs args)
|
||||||
|
{
|
||||||
|
var nodes = new HashSet<Vector2i>(args.Path);
|
||||||
|
|
||||||
|
var result = GetBreadthPath(new BreadthPathArgs()
|
||||||
|
{
|
||||||
|
Start = args.Start,
|
||||||
|
Ends = new List<Vector2i>()
|
||||||
|
{
|
||||||
|
args.End,
|
||||||
|
},
|
||||||
|
TileCost = node =>
|
||||||
|
{
|
||||||
|
if (!nodes.Contains(node))
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
Normal file
89
Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.NPC.Pathfinding;
|
||||||
|
|
||||||
|
public sealed partial class PathfindingSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Widens the path by the specified amount.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<Vector2i> GetWiden(WidenArgs args, Random random)
|
||||||
|
{
|
||||||
|
var tiles = new HashSet<Vector2i>(args.Path.Count * 2);
|
||||||
|
var variance = (args.MaxWiden - args.MinWiden) / 2f + args.MinWiden;
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
foreach (var tile in args.Path)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
if (counter != args.TileSkip)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
counter = 0;
|
||||||
|
|
||||||
|
var center = new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
|
||||||
|
|
||||||
|
if (args.Square)
|
||||||
|
{
|
||||||
|
for (var x = -variance; x <= variance; x++)
|
||||||
|
{
|
||||||
|
for (var y = -variance; y <= variance; y++)
|
||||||
|
{
|
||||||
|
var neighbor = center + new Vector2(x, y);
|
||||||
|
|
||||||
|
tiles.Add(neighbor.Floored());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var x = -variance; x <= variance; x++)
|
||||||
|
{
|
||||||
|
for (var y = -variance; y <= variance; y++)
|
||||||
|
{
|
||||||
|
var offset = new Vector2(x, y);
|
||||||
|
|
||||||
|
if (offset.Length() > variance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = center + offset;
|
||||||
|
|
||||||
|
tiles.Add(neighbor.Floored());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variance += random.NextFloat(-args.Variance * args.TileSkip, args.Variance * args.TileSkip);
|
||||||
|
variance = Math.Clamp(variance, args.MinWiden, args.MaxWiden);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct WidenArgs()
|
||||||
|
{
|
||||||
|
public bool Square = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many tiles to skip between iterations., 1-in-n
|
||||||
|
/// </summary>
|
||||||
|
public int TileSkip = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum amount to vary per tile.
|
||||||
|
/// </summary>
|
||||||
|
public float Variance = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum width.
|
||||||
|
/// </summary>
|
||||||
|
public float MinWiden = 2f;
|
||||||
|
|
||||||
|
|
||||||
|
public float MaxWiden = 7f;
|
||||||
|
|
||||||
|
public List<Vector2i> Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,13 @@ public sealed partial class NPCSteeringSystem
|
|||||||
|
|
||||||
// Grab the target position, either the next path node or our end goal..
|
// Grab the target position, either the next path node or our end goal..
|
||||||
var targetCoordinates = GetTargetCoordinates(steering);
|
var targetCoordinates = GetTargetCoordinates(steering);
|
||||||
|
|
||||||
|
if (!targetCoordinates.IsValid(EntityManager))
|
||||||
|
{
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var needsPath = false;
|
var needsPath = false;
|
||||||
|
|
||||||
// If the next node is invalid then get new ones
|
// If the next node is invalid then get new ones
|
||||||
@@ -243,6 +250,14 @@ public sealed partial class NPCSteeringSystem
|
|||||||
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
|
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
|
||||||
// TODO: If it's the last node just grab the target instead.
|
// TODO: If it's the last node just grab the target instead.
|
||||||
targetCoordinates = GetTargetCoordinates(steering);
|
targetCoordinates = GetTargetCoordinates(steering);
|
||||||
|
|
||||||
|
if (!targetCoordinates.IsValid(EntityManager))
|
||||||
|
{
|
||||||
|
SetDirection(mover, steering, Vector2.Zero);
|
||||||
|
steering.Status = SteeringStatus.NoPath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
targetMap = targetCoordinates.ToMap(EntityManager, _transform);
|
targetMap = targetCoordinates.ToMap(EntityManager, _transform);
|
||||||
|
|
||||||
// Can't make it again.
|
// Can't make it again.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,138 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Parallax;
|
|
||||||
using Content.Shared.Parallax.Biomes;
|
|
||||||
using Content.Shared.Parallax.Biomes.Markers;
|
|
||||||
using Content.Shared.Procedural;
|
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
|
||||||
using Content.Shared.Random.Helpers;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
|
||||||
|
|
||||||
public sealed partial class DungeonJob
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Handles PostGen code for marker layers + biomes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private async Task PostGen(BiomePostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
|
|
||||||
{
|
|
||||||
if (_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
|
|
||||||
return;
|
|
||||||
|
|
||||||
biomeComp = _entManager.AddComponent<BiomeComponent>(gridUid);
|
|
||||||
var biomeSystem = _entManager.System<BiomeSystem>();
|
|
||||||
biomeSystem.SetTemplate(gridUid, biomeComp, _prototype.Index(postGen.BiomeTemplate));
|
|
||||||
var seed = random.Next();
|
|
||||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
|
||||||
|
|
||||||
foreach (var node in dungeon.RoomTiles)
|
|
||||||
{
|
|
||||||
// Need to set per-tile to override data.
|
|
||||||
if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, grid, out var tile))
|
|
||||||
{
|
|
||||||
_maps.SetTile(gridUid, grid, node, tile.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, grid, out var decals))
|
|
||||||
{
|
|
||||||
foreach (var decal in decals)
|
|
||||||
{
|
|
||||||
_decals.TryAddDecal(decal.ID, new EntityCoordinates(gridUid, decal.Position), out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (biomeSystem.TryGetEntity(node, biomeComp, grid, out var entityProto))
|
|
||||||
{
|
|
||||||
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
|
|
||||||
var xform = xformQuery.Get(ent);
|
|
||||||
|
|
||||||
if (!xform.Comp.Anchored)
|
|
||||||
{
|
|
||||||
_transform.AnchorEntity(ent, xform);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Engine bug with SpawnAtPosition
|
|
||||||
DebugTools.Assert(xform.Comp.Anchored);
|
|
||||||
}
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
ValidateResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
biomeComp.Enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PostGen(BiomeMarkerLayerPostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
|
|
||||||
{
|
|
||||||
if (!_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var biomeSystem = _entManager.System<BiomeSystem>();
|
|
||||||
var weightedRandom = _prototype.Index(postGen.MarkerTemplate);
|
|
||||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
|
||||||
var templates = new Dictionary<string, int>();
|
|
||||||
|
|
||||||
for (var i = 0; i < postGen.Count; i++)
|
|
||||||
{
|
|
||||||
var template = weightedRandom.Pick(random);
|
|
||||||
var count = templates.GetOrNew(template);
|
|
||||||
count++;
|
|
||||||
templates[template] = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (template, count) in templates)
|
|
||||||
{
|
|
||||||
var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
|
|
||||||
|
|
||||||
var bounds = new Box2i();
|
|
||||||
|
|
||||||
foreach (var tile in dungeon.RoomTiles)
|
|
||||||
{
|
|
||||||
bounds = bounds.UnionTile(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
ValidateResume();
|
|
||||||
|
|
||||||
biomeSystem.GetMarkerNodes(gridUid, biomeComp, grid, markerTemplate, true, bounds, count,
|
|
||||||
random, out var spawnSet, out var existing, false);
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
ValidateResume();
|
|
||||||
|
|
||||||
foreach (var ent in existing)
|
|
||||||
{
|
|
||||||
_entManager.DeleteEntity(ent);
|
|
||||||
}
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
ValidateResume();
|
|
||||||
|
|
||||||
foreach (var (node, mask) in spawnSet)
|
|
||||||
{
|
|
||||||
string? proto;
|
|
||||||
|
|
||||||
if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
|
|
||||||
{
|
|
||||||
proto = maskedProto;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
proto = markerTemplate.Prototype;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
|
|
||||||
var xform = xformQuery.Get(ent);
|
|
||||||
|
|
||||||
if (!xform.Comp.Anchored)
|
|
||||||
_transform.AnchorEntity(ent, xform);
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
ValidateResume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Construction;
|
|
||||||
using Robust.Shared.CPUJob.JobQueues;
|
|
||||||
using Content.Server.Decals;
|
|
||||||
using Content.Shared.Construction.EntitySystems;
|
|
||||||
using Content.Shared.Maps;
|
|
||||||
using Content.Shared.Procedural;
|
|
||||||
using Content.Shared.Procedural.DungeonGenerators;
|
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
|
||||||
using Content.Shared.Tag;
|
|
||||||
using Robust.Server.Physics;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
|
||||||
|
|
||||||
public sealed partial class DungeonJob : Job<Dungeon>
|
|
||||||
{
|
|
||||||
private readonly IEntityManager _entManager;
|
|
||||||
private readonly IMapManager _mapManager;
|
|
||||||
private readonly IPrototypeManager _prototype;
|
|
||||||
private readonly ITileDefinitionManager _tileDefManager;
|
|
||||||
|
|
||||||
private readonly AnchorableSystem _anchorable;
|
|
||||||
private readonly DecalSystem _decals;
|
|
||||||
private readonly DungeonSystem _dungeon;
|
|
||||||
private readonly EntityLookupSystem _lookup;
|
|
||||||
private readonly TagSystem _tag;
|
|
||||||
private readonly TileSystem _tile;
|
|
||||||
private readonly SharedMapSystem _maps;
|
|
||||||
private readonly SharedTransformSystem _transform;
|
|
||||||
|
|
||||||
private readonly DungeonConfigPrototype _gen;
|
|
||||||
private readonly int _seed;
|
|
||||||
private readonly Vector2i _position;
|
|
||||||
|
|
||||||
private readonly MapGridComponent _grid;
|
|
||||||
private readonly EntityUid _gridUid;
|
|
||||||
|
|
||||||
private readonly ISawmill _sawmill;
|
|
||||||
|
|
||||||
public DungeonJob(
|
|
||||||
ISawmill sawmill,
|
|
||||||
double maxTime,
|
|
||||||
IEntityManager entManager,
|
|
||||||
IMapManager mapManager,
|
|
||||||
IPrototypeManager prototype,
|
|
||||||
ITileDefinitionManager tileDefManager,
|
|
||||||
AnchorableSystem anchorable,
|
|
||||||
DecalSystem decals,
|
|
||||||
DungeonSystem dungeon,
|
|
||||||
EntityLookupSystem lookup,
|
|
||||||
TagSystem tag,
|
|
||||||
TileSystem tile,
|
|
||||||
SharedTransformSystem transform,
|
|
||||||
DungeonConfigPrototype gen,
|
|
||||||
MapGridComponent grid,
|
|
||||||
EntityUid gridUid,
|
|
||||||
int seed,
|
|
||||||
Vector2i position,
|
|
||||||
CancellationToken cancellation = default) : base(maxTime, cancellation)
|
|
||||||
{
|
|
||||||
_sawmill = sawmill;
|
|
||||||
_entManager = entManager;
|
|
||||||
_mapManager = mapManager;
|
|
||||||
_prototype = prototype;
|
|
||||||
_tileDefManager = tileDefManager;
|
|
||||||
|
|
||||||
_anchorable = anchorable;
|
|
||||||
_decals = decals;
|
|
||||||
_dungeon = dungeon;
|
|
||||||
_lookup = lookup;
|
|
||||||
_tag = tag;
|
|
||||||
_tile = tile;
|
|
||||||
_maps = _entManager.System<SharedMapSystem>();
|
|
||||||
_transform = transform;
|
|
||||||
|
|
||||||
_gen = gen;
|
|
||||||
_grid = grid;
|
|
||||||
_gridUid = gridUid;
|
|
||||||
_seed = seed;
|
|
||||||
_position = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<Dungeon?> Process()
|
|
||||||
{
|
|
||||||
Dungeon dungeon;
|
|
||||||
_sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
|
|
||||||
_grid.CanSplit = false;
|
|
||||||
|
|
||||||
switch (_gen.Generator)
|
|
||||||
{
|
|
||||||
case NoiseDunGen noise:
|
|
||||||
dungeon = await GenerateNoiseDungeon(noise, _gridUid, _grid, _seed);
|
|
||||||
break;
|
|
||||||
case PrefabDunGen prefab:
|
|
||||||
dungeon = await GeneratePrefabDungeon(prefab, _gridUid, _grid, _seed);
|
|
||||||
DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTools.Assert(dungeon.RoomTiles.Count > 0);
|
|
||||||
|
|
||||||
// To make it slightly more deterministic treat this RNG as separate ig.
|
|
||||||
var random = new Random(_seed);
|
|
||||||
|
|
||||||
foreach (var post in _gen.PostGeneration)
|
|
||||||
{
|
|
||||||
_sawmill.Debug($"Doing postgen {post.GetType()} for {_gen.ID} with seed {_seed}");
|
|
||||||
|
|
||||||
switch (post)
|
|
||||||
{
|
|
||||||
case AutoCablingPostGen cabling:
|
|
||||||
await PostGen(cabling, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case BiomePostGen biome:
|
|
||||||
await PostGen(biome, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case BoundaryWallPostGen boundary:
|
|
||||||
await PostGen(boundary, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case CornerClutterPostGen clutter:
|
|
||||||
await PostGen(clutter, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case CorridorClutterPostGen corClutter:
|
|
||||||
await PostGen(corClutter, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case CorridorPostGen cordor:
|
|
||||||
await PostGen(cordor, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case CorridorDecalSkirtingPostGen decks:
|
|
||||||
await PostGen(decks, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case EntranceFlankPostGen flank:
|
|
||||||
await PostGen(flank, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case JunctionPostGen junc:
|
|
||||||
await PostGen(junc, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case MiddleConnectionPostGen dordor:
|
|
||||||
await PostGen(dordor, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case DungeonEntrancePostGen entrance:
|
|
||||||
await PostGen(entrance, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case ExternalWindowPostGen externalWindow:
|
|
||||||
await PostGen(externalWindow, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case InternalWindowPostGen internalWindow:
|
|
||||||
await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case BiomeMarkerLayerPostGen markerPost:
|
|
||||||
await PostGen(markerPost, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case RoomEntrancePostGen rEntrance:
|
|
||||||
await PostGen(rEntrance, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case WallMountPostGen wall:
|
|
||||||
await PostGen(wall, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
case WormCorridorPostGen worm:
|
|
||||||
await PostGen(worm, dungeon, _gridUid, _grid, random);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
|
||||||
|
|
||||||
if (!ValidateResume())
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
|
|
||||||
_grid.CanSplit = true;
|
|
||||||
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
|
|
||||||
return dungeon;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateResume()
|
|
||||||
{
|
|
||||||
if (_entManager.Deleted(_gridUid))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ExteriorDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<Dungeon>> GenerateExteriorDungen(Vector2i position, ExteriorDunGen dungen, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
DebugTools.Assert(_grid.ChunkCount > 0);
|
||||||
|
|
||||||
|
var aabb = new Box2i(_grid.LocalAABB.BottomLeft.Floored(), _grid.LocalAABB.TopRight.Floored());
|
||||||
|
var angle = random.NextAngle();
|
||||||
|
|
||||||
|
var distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f);
|
||||||
|
|
||||||
|
var startTile = new Vector2i(0, (int) distance).Rotate(angle);
|
||||||
|
|
||||||
|
Vector2i? dungeonSpawn = null;
|
||||||
|
var pathfinder = _entManager.System<PathfindingSystem>();
|
||||||
|
|
||||||
|
// Gridcast
|
||||||
|
pathfinder.GridCast(startTile, position, tile =>
|
||||||
|
{
|
||||||
|
if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
|
||||||
|
tileRef.Tile.IsSpace(_tileDefManager))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dungeonSpawn = tile;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dungeonSpawn == null)
|
||||||
|
{
|
||||||
|
return new List<Dungeon>()
|
||||||
|
{
|
||||||
|
Dungeon.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = _prototype.Index(dungen.Proto);
|
||||||
|
var nextSeed = random.Next();
|
||||||
|
var dungeons = await GetDungeons(dungeonSpawn.Value, config, config.Data, config.Layers, reservedTiles, nextSeed, new Random(nextSeed));
|
||||||
|
|
||||||
|
return dungeons;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="FillGridDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> GenerateFillDunGen(DungeonData data, HashSet<Vector2i> reservedTiles)
|
||||||
|
{
|
||||||
|
if (!data.Entities.TryGetValue(DungeonDataKey.Fill, out var fillEnt))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(FillGridDunGen));
|
||||||
|
return Dungeon.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomTiles = new HashSet<Vector2i>();
|
||||||
|
var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
|
||||||
|
|
||||||
|
while (tiles.MoveNext(out var tileRef))
|
||||||
|
{
|
||||||
|
var tile = tileRef.Value.GridIndices;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(tile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
|
||||||
|
_entManager.SpawnEntity(fillEnt, gridPos);
|
||||||
|
|
||||||
|
roomTiles.Add(tile);
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dungeon = new Dungeon();
|
||||||
|
var room = new DungeonRoom(roomTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
|
||||||
|
dungeon.AddRoom(room);
|
||||||
|
|
||||||
|
return dungeon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,25 @@ using Content.Shared.Maps;
|
|||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Procedural.DungeonGenerators;
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
public sealed partial class DungeonJob
|
public sealed partial class DungeonJob
|
||||||
{
|
{
|
||||||
private async Task<Dungeon> GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid,
|
/// <summary>
|
||||||
int seed)
|
/// <see cref="NoiseDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> GenerateNoiseDunGen(
|
||||||
|
Vector2i position,
|
||||||
|
NoiseDunGen dungen,
|
||||||
|
HashSet<Vector2i> reservedTiles,
|
||||||
|
int seed,
|
||||||
|
Random random)
|
||||||
{
|
{
|
||||||
var rand = new Random(seed);
|
|
||||||
var tiles = new List<(Vector2i, Tile)>();
|
var tiles = new List<(Vector2i, Tile)>();
|
||||||
|
var matrix = Matrix3Helpers.CreateTranslation(position);
|
||||||
|
|
||||||
foreach (var layer in dungen.Layers)
|
foreach (var layer in dungen.Layers)
|
||||||
{
|
{
|
||||||
@@ -30,7 +36,7 @@ public sealed partial class DungeonJob
|
|||||||
var frontier = new Queue<Vector2i>();
|
var frontier = new Queue<Vector2i>();
|
||||||
var rooms = new List<DungeonRoom>();
|
var rooms = new List<DungeonRoom>();
|
||||||
var tileCount = 0;
|
var tileCount = 0;
|
||||||
var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd);
|
var tileCap = random.NextGaussian(dungen.TileCap, dungen.CapStd);
|
||||||
var visited = new HashSet<Vector2i>();
|
var visited = new HashSet<Vector2i>();
|
||||||
|
|
||||||
while (iterations > 0 && tileCount < tileCap)
|
while (iterations > 0 && tileCount < tileCap)
|
||||||
@@ -39,22 +45,22 @@ public sealed partial class DungeonJob
|
|||||||
iterations--;
|
iterations--;
|
||||||
|
|
||||||
// Get a random exterior tile to start floodfilling from.
|
// Get a random exterior tile to start floodfilling from.
|
||||||
var edge = rand.Next(4);
|
var edge = random.Next(4);
|
||||||
Vector2i seedTile;
|
Vector2i seedTile;
|
||||||
|
|
||||||
switch (edge)
|
switch (edge)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
|
seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
seedTile = new Vector2i(area.Right + 1, rand.Next(area.Bottom - 2, area.Top + 1));
|
seedTile = new Vector2i(area.Right + 1, random.Next(area.Bottom - 2, area.Top + 1));
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Top + 1);
|
seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Top + 1);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
seedTile = new Vector2i(area.Left - 2, rand.Next(area.Bottom - 2, area.Top + 1));
|
seedTile = new Vector2i(area.Left - 2, random.Next(area.Bottom - 2, area.Top + 1));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
@@ -80,14 +86,20 @@ public sealed partial class DungeonJob
|
|||||||
if (value < layer.Threshold)
|
if (value < layer.Threshold)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
roomArea = roomArea.UnionTile(node);
|
|
||||||
foundNoise = true;
|
foundNoise = true;
|
||||||
noiseFill = true;
|
noiseFill = true;
|
||||||
var tileDef = _tileDefManager[layer.Tile];
|
|
||||||
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, rand);
|
|
||||||
|
|
||||||
tiles.Add((node, new Tile(tileDef.TileId, variant: variant)));
|
// Still want the tile to gen as normal but can't do anything with it.
|
||||||
roomTiles.Add(node);
|
if (reservedTiles.Contains(node))
|
||||||
|
break;
|
||||||
|
|
||||||
|
roomArea = roomArea.UnionTile(node);
|
||||||
|
var tileDef = _tileDefManager[layer.Tile];
|
||||||
|
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
|
||||||
|
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
|
||||||
|
|
||||||
|
tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
|
||||||
|
roomTiles.Add(adjusted);
|
||||||
tileCount++;
|
tileCount++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,7 +135,7 @@ public sealed partial class DungeonJob
|
|||||||
|
|
||||||
foreach (var tile in roomTiles)
|
foreach (var tile in roomTiles)
|
||||||
{
|
{
|
||||||
center += tile + grid.TileSizeHalfVector;
|
center += tile + _grid.TileSizeHalfVector;
|
||||||
}
|
}
|
||||||
|
|
||||||
center /= roomTiles.Count;
|
center /= roomTiles.Count;
|
||||||
@@ -132,15 +144,8 @@ public sealed partial class DungeonJob
|
|||||||
ValidateResume();
|
ValidateResume();
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.SetTiles(tiles);
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
|
||||||
var dungeon = new Dungeon(rooms);
|
var dungeon = new Dungeon(rooms);
|
||||||
|
|
||||||
foreach (var tile in tiles)
|
|
||||||
{
|
|
||||||
dungeon.RoomTiles.Add(tile.Item1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dungeon;
|
return dungeon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.Distance;
|
||||||
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* See https://www.redblobgames.com/maps/terrain-from-noise/#islands
|
||||||
|
* Really it's just blending from the original noise (which may occupy the entire area)
|
||||||
|
* with some other shape to confine it into a bounds more naturally.
|
||||||
|
* https://old.reddit.com/r/proceduralgeneration/comments/kaen7h/new_video_on_procedural_island_noise_generation/gfjmgen/ also has more variations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="NoiseDistanceDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> GenerateNoiseDistanceDunGen(
|
||||||
|
Vector2i position,
|
||||||
|
NoiseDistanceDunGen dungen,
|
||||||
|
HashSet<Vector2i> reservedTiles,
|
||||||
|
int seed,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
var tiles = new List<(Vector2i, Tile)>();
|
||||||
|
var matrix = Matrix3Helpers.CreateTranslation(position);
|
||||||
|
|
||||||
|
foreach (var layer in dungen.Layers)
|
||||||
|
{
|
||||||
|
layer.Noise.SetSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First we have to find a seed tile, then floodfill from there until we get to noise
|
||||||
|
// at which point we floodfill the entire noise.
|
||||||
|
var area = Box2i.FromDimensions(-dungen.Size / 2, dungen.Size);
|
||||||
|
var roomTiles = new HashSet<Vector2i>();
|
||||||
|
var width = (float) area.Width;
|
||||||
|
var height = (float) area.Height;
|
||||||
|
|
||||||
|
for (var x = area.Left; x <= area.Right; x++)
|
||||||
|
{
|
||||||
|
for (var y = area.Bottom; y <= area.Top; y++)
|
||||||
|
{
|
||||||
|
var node = new Vector2i(x, y);
|
||||||
|
|
||||||
|
foreach (var layer in dungen.Layers)
|
||||||
|
{
|
||||||
|
var value = layer.Noise.GetNoise(node.X, node.Y);
|
||||||
|
|
||||||
|
if (dungen.DistanceConfig != null)
|
||||||
|
{
|
||||||
|
// Need to get dx - dx in a range from -1 -> 1
|
||||||
|
var dx = 2 * x / width;
|
||||||
|
var dy = 2 * y / height;
|
||||||
|
|
||||||
|
var distance = GetDistance(dx, dy, dungen.DistanceConfig);
|
||||||
|
|
||||||
|
value = MathHelper.Lerp(value, 1f - distance, dungen.DistanceConfig.BlendWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < layer.Threshold)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var tileDef = _tileDefManager[layer.Tile];
|
||||||
|
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
|
||||||
|
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
|
||||||
|
|
||||||
|
// Do this down here because noise has a much higher chance of failing than reserved tiles.
|
||||||
|
if (reservedTiles.Contains(adjusted))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
|
||||||
|
roomTiles.Add(adjusted);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
}
|
||||||
|
|
||||||
|
var room = new DungeonRoom(roomTiles, area.Center, area, new HashSet<Vector2i>());
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
var dungeon = new Dungeon(new List<DungeonRoom>()
|
||||||
|
{
|
||||||
|
room,
|
||||||
|
});
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
return dungeon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetDistance(float dx, float dy, IDunGenDistance distance)
|
||||||
|
{
|
||||||
|
switch (distance)
|
||||||
|
{
|
||||||
|
case DunGenEuclideanSquaredDistance:
|
||||||
|
return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2));
|
||||||
|
case DunGenSquareBump:
|
||||||
|
return 1f - (1f - dx * dx) * (1f - dy * dy);
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Shared.Decals;
|
|
||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Procedural.DungeonGenerators;
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
public sealed partial class DungeonJob
|
public sealed partial class DungeonJob
|
||||||
{
|
{
|
||||||
private async Task<Dungeon> GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid gridUid, MapGridComponent grid, int seed)
|
/// <summary>
|
||||||
|
/// <see cref="PrefabDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> GeneratePrefabDunGen(Vector2i position, DungeonData data, PrefabDunGen prefab, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
{
|
{
|
||||||
var random = new Random(seed);
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
|
!data.Whitelists.TryGetValue(DungeonDataKey.Rooms, out var roomWhitelist))
|
||||||
var gen = _prototype.Index<DungeonPresetPrototype>(preset);
|
{
|
||||||
|
LogDataError(typeof(PrefabDunGen));
|
||||||
|
return Dungeon.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
|
var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
|
||||||
var dungeonTransform = Matrix3Helpers.CreateTransform(_position, dungeonRotation);
|
var gen = _prototype.Index(preset);
|
||||||
|
|
||||||
|
var dungeonRotation = _dungeon.GetDungeonRotation(random.Next());
|
||||||
|
var dungeonTransform = Matrix3Helpers.CreateTransform(position, dungeonRotation);
|
||||||
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
|
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
|
||||||
|
|
||||||
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
|
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
|
||||||
@@ -42,12 +50,15 @@ public sealed partial class DungeonJob
|
|||||||
{
|
{
|
||||||
var whitelisted = false;
|
var whitelisted = false;
|
||||||
|
|
||||||
foreach (var tag in prefab.RoomWhitelist)
|
if (roomWhitelist?.Tags != null)
|
||||||
{
|
{
|
||||||
if (proto.Tags.Contains(tag))
|
foreach (var tag in roomWhitelist.Tags)
|
||||||
{
|
{
|
||||||
whitelisted = true;
|
if (proto.Tags.Contains(tag))
|
||||||
break;
|
{
|
||||||
|
whitelisted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,12 +193,16 @@ public sealed partial class DungeonJob
|
|||||||
{
|
{
|
||||||
for (var y = roomSize.Bottom; y < roomSize.Top; y++)
|
for (var y = roomSize.Bottom; y < roomSize.Top; y++)
|
||||||
{
|
{
|
||||||
var index = Vector2.Transform(new Vector2(x, y) + grid.TileSizeHalfVector - packCenter, matty).Floored();
|
var index = Vector2.Transform(new Vector2(x, y) + _grid.TileSizeHalfVector - packCenter, matty).Floored();
|
||||||
tiles.Add((index, new Tile(_tileDefManager["FloorPlanetGrass"].TileId)));
|
|
||||||
|
if (reservedTiles.Contains(index))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.Add((index, new Tile(_tileDefManager[tileProto].TileId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.SetTiles(tiles);
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
tiles.Clear();
|
tiles.Clear();
|
||||||
_sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
|
_sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
|
||||||
continue;
|
continue;
|
||||||
@@ -215,12 +230,12 @@ public sealed partial class DungeonJob
|
|||||||
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
|
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
|
||||||
|
|
||||||
// The expensive bit yippy.
|
// The expensive bit yippy.
|
||||||
_dungeon.SpawnRoom(gridUid, grid, dungeonMatty, room);
|
_dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
|
||||||
|
|
||||||
var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
|
var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize;
|
||||||
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
|
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
|
||||||
var exterior = new HashSet<Vector2i>(room.Size.X * 2 + room.Size.Y * 2);
|
var exterior = new HashSet<Vector2i>(room.Size.X * 2 + room.Size.Y * 2);
|
||||||
var tileOffset = -roomCenter + grid.TileSizeHalfVector;
|
var tileOffset = -roomCenter + _grid.TileSizeHalfVector;
|
||||||
Box2i? mapBounds = null;
|
Box2i? mapBounds = null;
|
||||||
|
|
||||||
for (var x = -1; x <= room.Size.X; x++)
|
for (var x = -1; x <= room.Size.X; x++)
|
||||||
@@ -232,8 +247,12 @@ public sealed partial class DungeonJob
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty);
|
var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty).Floored();
|
||||||
exterior.Add(tilePos.Floored());
|
|
||||||
|
if (reservedTiles.Contains(tilePos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
exterior.Add(tilePos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,16 +268,18 @@ public sealed partial class DungeonJob
|
|||||||
roomTiles.Add(tileIndex);
|
roomTiles.Add(tileIndex);
|
||||||
|
|
||||||
mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex);
|
mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex);
|
||||||
center += tilePos + grid.TileSizeHalfVector;
|
center += tilePos + _grid.TileSizeHalfVector;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
center /= roomTiles.Count;
|
center /= roomTiles.Count;
|
||||||
|
|
||||||
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
|
dungeon.AddRoom(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
await SuspendDungeon();
|
||||||
ValidateResume();
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return Dungeon.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,20 +288,16 @@ public sealed partial class DungeonJob
|
|||||||
|
|
||||||
foreach (var room in dungeon.Rooms)
|
foreach (var room in dungeon.Rooms)
|
||||||
{
|
{
|
||||||
dungeon.RoomTiles.UnionWith(room.Tiles);
|
dungeonCenter += room.Center;
|
||||||
dungeon.RoomExteriorTiles.UnionWith(room.Exterior);
|
SetDungeonEntrance(dungeon, room, reservedTiles, random);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var room in dungeon.Rooms)
|
dungeon.Rebuild();
|
||||||
{
|
|
||||||
dungeonCenter += room.Center;
|
|
||||||
SetDungeonEntrance(dungeon, room, random);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dungeon;
|
return dungeon;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, Random random)
|
private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
{
|
{
|
||||||
// TODO: Move to dungeonsystem.
|
// TODO: Move to dungeonsystem.
|
||||||
|
|
||||||
@@ -323,8 +340,10 @@ public sealed partial class DungeonJob
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(entrancePos))
|
||||||
|
continue;
|
||||||
|
|
||||||
room.Entrances.Add(entrancePos);
|
room.Entrances.Add(entrancePos);
|
||||||
dungeon.Entrances.Add(entrancePos);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ReplaceTileDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> GenerateTileReplacementDunGen(ReplaceTileDunGen gen, DungeonData data, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
|
||||||
|
var replacements = new List<(Vector2i Index, Tile Tile)>();
|
||||||
|
var reserved = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
while (tiles.MoveNext(out var tileRef))
|
||||||
|
{
|
||||||
|
var node = tileRef.Value.GridIndices;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var layer in gen.Layers)
|
||||||
|
{
|
||||||
|
var value = layer.Noise.GetNoise(node.X, node.Y);
|
||||||
|
|
||||||
|
if (value < layer.Threshold)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Tile tile;
|
||||||
|
|
||||||
|
if (random.Prob(gen.VariantWeight))
|
||||||
|
{
|
||||||
|
tile = _tileDefManager.GetVariantTile(_prototype.Index(layer.Tile), random);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tile = new Tile(_prototype.Index(layer.Tile).TileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements.Add((node, tile));
|
||||||
|
reserved.Add(node);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, replacements);
|
||||||
|
return new Dungeon(new List<DungeonRoom>()
|
||||||
|
{
|
||||||
|
new DungeonRoom(reserved, _position, Box2i.Empty, new HashSet<Vector2i>()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs
Normal file
58
Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Ghost.Roles.Components;
|
||||||
|
using Content.Server.NPC.Components;
|
||||||
|
using Content.Server.NPC.Systems;
|
||||||
|
using Content.Shared.Physics;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.DungeonLayers;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
private async Task PostGen(
|
||||||
|
MobsDunGen gen,
|
||||||
|
Dungeon dungeon,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
var availableRooms = new ValueList<DungeonRoom>();
|
||||||
|
availableRooms.AddRange(dungeon.Rooms);
|
||||||
|
var availableTiles = new ValueList<Vector2i>(dungeon.AllTiles);
|
||||||
|
|
||||||
|
var entities = EntitySpawnCollection.GetSpawns(gen.Groups, random);
|
||||||
|
var count = random.Next(gen.MinCount, gen.MaxCount + 1);
|
||||||
|
var npcs = _entManager.System<NPCSystem>();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
while (availableTiles.Count > 0)
|
||||||
|
{
|
||||||
|
var tile = availableTiles.RemoveSwap(random.Next(availableTiles.Count));
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, tile, (int) CollisionGroup.MachineLayer,
|
||||||
|
(int) CollisionGroup.MachineLayer))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ent in entities)
|
||||||
|
{
|
||||||
|
var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
|
||||||
|
_entManager.RemoveComponent<GhostRoleComponent>(uid);
|
||||||
|
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
|
||||||
|
npcs.SleepNPC(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs
Normal file
149
Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.Components;
|
||||||
|
using Content.Shared.Procedural.DungeonLayers;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="OreDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(
|
||||||
|
OreDunGen gen,
|
||||||
|
Dungeon dungeon,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
// Doesn't use dungeon data because layers and we don't need top-down support at the moment.
|
||||||
|
|
||||||
|
var emptyTiles = false;
|
||||||
|
var replaceEntities = new Dictionary<Vector2i, EntityUid>();
|
||||||
|
var availableTiles = new List<Vector2i>();
|
||||||
|
|
||||||
|
foreach (var node in dungeon.AllTiles)
|
||||||
|
{
|
||||||
|
// Empty tile, skip if relevant.
|
||||||
|
if (!emptyTiles && (!_maps.TryGetTile(_grid, node, out var tile) || tile.IsEmpty))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if it's a valid spawn, if so then use it.
|
||||||
|
var enumerator = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, node);
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
// We use existing entities as a mark to spawn in place
|
||||||
|
// OR
|
||||||
|
// We check for any existing entities to see if we can spawn there.
|
||||||
|
while (enumerator.MoveNext(out var uid))
|
||||||
|
{
|
||||||
|
// We can't replace so just stop here.
|
||||||
|
if (gen.Replacement == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
|
||||||
|
|
||||||
|
if (prototype?.ID == gen.Replacement)
|
||||||
|
{
|
||||||
|
replaceEntities[node] = uid.Value;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Add it to valid nodes.
|
||||||
|
availableTiles.Add(node);
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remapping = new Dictionary<EntProtoId, EntProtoId>();
|
||||||
|
|
||||||
|
// TODO: Move this to engine
|
||||||
|
if (_prototype.TryIndex(gen.Entity, out var proto) &&
|
||||||
|
proto.Components.TryGetComponent("EntityRemap", out var comps))
|
||||||
|
{
|
||||||
|
var remappingComp = (EntityRemapComponent) comps;
|
||||||
|
remapping = remappingComp.Mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frontier = new ValueList<Vector2i>(32);
|
||||||
|
|
||||||
|
// Iterate the group counts and pathfind out each group.
|
||||||
|
for (var i = 0; i < gen.Count; i++)
|
||||||
|
{
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
|
||||||
|
|
||||||
|
// While we have remaining tiles keep iterating
|
||||||
|
while (groupSize >= 0 && availableTiles.Count > 0)
|
||||||
|
{
|
||||||
|
var startNode = random.PickAndTake(availableTiles);
|
||||||
|
frontier.Clear();
|
||||||
|
frontier.Add(startNode);
|
||||||
|
|
||||||
|
// This essentially may lead to a vein being split in multiple areas but the count matters more than position.
|
||||||
|
while (frontier.Count > 0 && groupSize >= 0)
|
||||||
|
{
|
||||||
|
// Need to pick a random index so we don't just get straight lines of ores.
|
||||||
|
var frontierIndex = random.Next(frontier.Count);
|
||||||
|
var node = frontier[frontierIndex];
|
||||||
|
frontier.RemoveSwap(frontierIndex);
|
||||||
|
availableTiles.Remove(node);
|
||||||
|
|
||||||
|
// Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = new Vector2i(node.X + x, node.Y + y);
|
||||||
|
|
||||||
|
if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
frontier.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var prototype = gen.Entity;
|
||||||
|
|
||||||
|
if (replaceEntities.TryGetValue(node, out var existingEnt))
|
||||||
|
{
|
||||||
|
var existingProto = _entManager.GetComponent<MetaDataComponent>(existingEnt).EntityPrototype;
|
||||||
|
_entManager.DeleteEntity(existingEnt);
|
||||||
|
|
||||||
|
if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
|
||||||
|
{
|
||||||
|
prototype = remapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile valid salad so add it.
|
||||||
|
_entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
|
||||||
|
|
||||||
|
groupSize--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupSize > 0)
|
||||||
|
{
|
||||||
|
_sawmill.Warning($"Found remaining group size for ore veins!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs
Normal file
134
Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Run after the main dungeon generation
|
||||||
|
*/
|
||||||
|
|
||||||
|
private bool HasWall(Vector2i tile)
|
||||||
|
{
|
||||||
|
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
|
||||||
|
|
||||||
|
while (anchored.MoveNext(out var uid))
|
||||||
|
{
|
||||||
|
if (_tags.HasTag(uid.Value, "Wall"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildCorridorExterior(Dungeon dungeon)
|
||||||
|
{
|
||||||
|
var exterior = dungeon.CorridorExteriorTiles;
|
||||||
|
|
||||||
|
// Just ignore entrances or whatever for now.
|
||||||
|
foreach (var tile in dungeon.CorridorTiles)
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
var neighbor = new Vector2i(tile.X + x, tile.Y + y);
|
||||||
|
|
||||||
|
if (dungeon.CorridorTiles.Contains(neighbor) ||
|
||||||
|
dungeon.RoomExteriorTiles.Contains(neighbor) ||
|
||||||
|
dungeon.RoomTiles.Contains(neighbor) ||
|
||||||
|
dungeon.Entrances.Contains(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
exterior.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)
|
||||||
|
{
|
||||||
|
var expansion = width - 2;
|
||||||
|
|
||||||
|
// Widen the path
|
||||||
|
if (expansion >= 1)
|
||||||
|
{
|
||||||
|
var toAdd = new ValueList<Vector2i>();
|
||||||
|
|
||||||
|
foreach (var node in corridorTiles)
|
||||||
|
{
|
||||||
|
// Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug
|
||||||
|
// exterior walls and make the path smaller.
|
||||||
|
|
||||||
|
for (var x = -expansion; x <= expansion; x++)
|
||||||
|
{
|
||||||
|
for (var y = -expansion; y <= expansion; y++)
|
||||||
|
{
|
||||||
|
var neighbor = new Vector2(node.X + x, node.Y + y).Floored();
|
||||||
|
|
||||||
|
// Diagonals still matter here.
|
||||||
|
if (dungeon.RoomTiles.Contains(neighbor) ||
|
||||||
|
dungeon.RoomExteriorTiles.Contains(neighbor))
|
||||||
|
{
|
||||||
|
// Try
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdd.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in toAdd)
|
||||||
|
{
|
||||||
|
corridorTiles.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes any unwanted obstacles around a door tile.
|
||||||
|
/// </summary>
|
||||||
|
private void ClearDoor(Dungeon dungeon, MapGridComponent grid, Vector2i indices, bool strict = false)
|
||||||
|
{
|
||||||
|
var flags = strict
|
||||||
|
? LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries
|
||||||
|
: LookupFlags.Dynamic | LookupFlags.Static;
|
||||||
|
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = new Vector2i(indices.X + x, indices.Y + y);
|
||||||
|
|
||||||
|
if (!dungeon.RoomTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Shrink by 0.01 to avoid polygon overlap from neighboring tiles.
|
||||||
|
// TODO: Uhh entityset re-usage.
|
||||||
|
foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, new Box2(neighbor * grid.TileSize, (neighbor + 1) * grid.TileSize).Enlarged(-0.1f), flags))
|
||||||
|
{
|
||||||
|
if (!_physicsQuery.TryGetComponent(ent, out var physics) ||
|
||||||
|
!physics.Hard ||
|
||||||
|
(DungeonSystem.CollisionMask & physics.CollisionLayer) == 0x0 &&
|
||||||
|
(DungeonSystem.CollisionLayer & physics.CollisionMask) == 0x0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_entManager.DeleteEntity(ent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.NodeContainer;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="AutoCablingDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(AutoCablingDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Entities.TryGetValue(DungeonDataKey.Cabling, out var ent))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(AutoCablingDunGen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's a lot of ways you could do this.
|
||||||
|
// For now we'll just connect every LV cable in the dungeon.
|
||||||
|
var cableTiles = new HashSet<Vector2i>();
|
||||||
|
var allTiles = new HashSet<Vector2i>(dungeon.CorridorTiles);
|
||||||
|
allTiles.UnionWith(dungeon.RoomTiles);
|
||||||
|
allTiles.UnionWith(dungeon.RoomExteriorTiles);
|
||||||
|
allTiles.UnionWith(dungeon.CorridorExteriorTiles);
|
||||||
|
var nodeQuery = _entManager.GetEntityQuery<NodeContainerComponent>();
|
||||||
|
|
||||||
|
// Gather existing nodes
|
||||||
|
foreach (var tile in allTiles)
|
||||||
|
{
|
||||||
|
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
|
||||||
|
|
||||||
|
while (anchored.MoveNext(out var anc))
|
||||||
|
{
|
||||||
|
if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
|
||||||
|
!nodeContainer.Nodes.ContainsKey("power"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cableTiles.Add(tile);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterating them all might be expensive.
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var startNodes = new List<Vector2i>(cableTiles);
|
||||||
|
random.Shuffle(startNodes);
|
||||||
|
var start = startNodes[0];
|
||||||
|
var remaining = new HashSet<Vector2i>(startNodes);
|
||||||
|
var frontier = new PriorityQueue<Vector2i, float>();
|
||||||
|
frontier.Enqueue(start, 0f);
|
||||||
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
||||||
|
var costSoFar = new Dictionary<Vector2i, float>();
|
||||||
|
var lastDirection = new Dictionary<Vector2i, Direction>();
|
||||||
|
costSoFar[start] = 0f;
|
||||||
|
lastDirection[start] = Direction.Invalid;
|
||||||
|
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
if (frontier.Count == 0)
|
||||||
|
{
|
||||||
|
var newStart = remaining.First();
|
||||||
|
frontier.Enqueue(newStart, 0f);
|
||||||
|
lastDirection[newStart] = Direction.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = frontier.Dequeue();
|
||||||
|
|
||||||
|
if (remaining.Remove(node))
|
||||||
|
{
|
||||||
|
var weh = node;
|
||||||
|
|
||||||
|
while (cameFrom.TryGetValue(weh, out var receiver))
|
||||||
|
{
|
||||||
|
cableTiles.Add(weh);
|
||||||
|
weh = receiver;
|
||||||
|
|
||||||
|
if (weh == start)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_maps.TryGetTileRef(_gridUid, _grid, node, out var tileRef) || tileRef.Tile.IsEmpty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) (i * 2);
|
||||||
|
|
||||||
|
var neighbor = node + dir.ToIntVec();
|
||||||
|
var tileCost = 1f;
|
||||||
|
|
||||||
|
// Prefer straight lines.
|
||||||
|
if (lastDirection[node] != dir)
|
||||||
|
{
|
||||||
|
tileCost *= 1.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cableTiles.Contains(neighbor))
|
||||||
|
{
|
||||||
|
tileCost *= 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer tiles without walls on them
|
||||||
|
if (HasWall(neighbor))
|
||||||
|
{
|
||||||
|
tileCost *= 20f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gScore = costSoFar[node] + tileCost;
|
||||||
|
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
lastDirection[neighbor] = dir;
|
||||||
|
frontier.Enqueue(neighbor, gScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tile in cableTiles)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(tile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
while (anchored.MoveNext(out var anc))
|
||||||
|
{
|
||||||
|
if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
|
||||||
|
!nodeContainer.Nodes.ContainsKey("power"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_entManager.SpawnEntity(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Parallax;
|
||||||
|
using Content.Shared.Parallax.Biomes;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="BiomeDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (_entManager.TryGetComponent(_gridUid, out BiomeComponent? biomeComp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
biomeComp = _entManager.AddComponent<BiomeComponent>(_gridUid);
|
||||||
|
var biomeSystem = _entManager.System<BiomeSystem>();
|
||||||
|
biomeSystem.SetTemplate(_gridUid, biomeComp, _prototype.Index(dunGen.BiomeTemplate));
|
||||||
|
var seed = random.Next();
|
||||||
|
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
foreach (var node in dungeon.RoomTiles)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Need to set per-tile to override data.
|
||||||
|
if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, _grid, out var tile))
|
||||||
|
{
|
||||||
|
_maps.SetTile(_gridUid, _grid, node, tile.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, _grid, out var decals))
|
||||||
|
{
|
||||||
|
foreach (var decal in decals)
|
||||||
|
{
|
||||||
|
_decals.TryAddDecal(decal.ID, new EntityCoordinates(_gridUid, decal.Position), out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (biomeSystem.TryGetEntity(node, biomeComp, _grid, out var entityProto))
|
||||||
|
{
|
||||||
|
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
|
||||||
|
var xform = xformQuery.Get(ent);
|
||||||
|
|
||||||
|
if (!xform.Comp.Anchored)
|
||||||
|
{
|
||||||
|
_transform.AnchorEntity(ent, xform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Engine bug with SpawnAtPosition
|
||||||
|
DebugTools.Assert(xform.Comp.Anchored);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
biomeComp.Enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Parallax;
|
||||||
|
using Content.Shared.Parallax.Biomes;
|
||||||
|
using Content.Shared.Parallax.Biomes.Markers;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Random.Helpers;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="BiomeMarkerLayerDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(BiomeMarkerLayerDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
// If we're adding biome then disable it and just use for markers.
|
||||||
|
if (_entManager.EnsureComponent(_gridUid, out BiomeComponent biomeComp))
|
||||||
|
{
|
||||||
|
biomeComp.Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var biomeSystem = _entManager.System<BiomeSystem>();
|
||||||
|
var weightedRandom = _prototype.Index(dunGen.MarkerTemplate);
|
||||||
|
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||||
|
var templates = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
for (var i = 0; i < dunGen.Count; i++)
|
||||||
|
{
|
||||||
|
var template = weightedRandom.Pick(random);
|
||||||
|
var count = templates.GetOrNew(template);
|
||||||
|
count++;
|
||||||
|
templates[template] = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (template, count) in templates)
|
||||||
|
{
|
||||||
|
var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
|
||||||
|
|
||||||
|
var bounds = new Box2i();
|
||||||
|
|
||||||
|
foreach (var tile in dungeon.RoomTiles)
|
||||||
|
{
|
||||||
|
bounds = bounds.UnionTile(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
|
||||||
|
biomeSystem.GetMarkerNodes(_gridUid, biomeComp, _grid, markerTemplate, true, bounds, count,
|
||||||
|
random, out var spawnSet, out var existing, false);
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var checkTile = reservedTiles.Count > 0;
|
||||||
|
|
||||||
|
foreach (var ent in existing)
|
||||||
|
{
|
||||||
|
if (checkTile && reservedTiles.Contains(_maps.LocalToTile(_gridUid, _grid, _xformQuery.GetComponent(ent).Coordinates)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_entManager.DeleteEntity(ent);
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (node, mask) in spawnSet)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string? proto;
|
||||||
|
|
||||||
|
if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
|
||||||
|
{
|
||||||
|
proto = maskedProto;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
proto = markerTemplate.Prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
|
||||||
|
var xform = xformQuery.Get(ent);
|
||||||
|
|
||||||
|
if (!xform.Comp.Anchored)
|
||||||
|
_transform.AnchorEntity(ent, xform);
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="BoundaryWallDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(BoundaryWallDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var protoTileDef) ||
|
||||||
|
!data.Entities.TryGetValue(DungeonDataKey.Walls, out var wall))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Error finding dungeon data for {nameof(gen)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileDef = _tileDefManager[protoTileDef];
|
||||||
|
var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
|
||||||
|
|
||||||
|
if (!data.Entities.TryGetValue(DungeonDataKey.CornerWalls, out var cornerWall))
|
||||||
|
{
|
||||||
|
cornerWall = wall;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cornerWall == default)
|
||||||
|
{
|
||||||
|
cornerWall = wall;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn wall outline
|
||||||
|
// - Tiles first
|
||||||
|
foreach (var neighbor in dungeon.RoomExteriorTiles)
|
||||||
|
{
|
||||||
|
DebugTools.Assert(!dungeon.RoomTiles.Contains(neighbor));
|
||||||
|
|
||||||
|
if (dungeon.Entrances.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var index in dungeon.CorridorExteriorTiles)
|
||||||
|
{
|
||||||
|
if (dungeon.RoomTiles.Contains(index))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
|
||||||
|
// Double iteration coz we bulk set tiles for speed.
|
||||||
|
for (var i = 0; i < tiles.Count; i++)
|
||||||
|
{
|
||||||
|
var index = tiles[i];
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, index.Index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If no cardinal neighbors in dungeon then we're a corner.
|
||||||
|
var isCorner = true;
|
||||||
|
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y);
|
||||||
|
|
||||||
|
if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor))
|
||||||
|
{
|
||||||
|
isCorner = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCorner)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCorner)
|
||||||
|
_entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
|
||||||
|
|
||||||
|
if (!isCorner)
|
||||||
|
_entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
|
||||||
|
|
||||||
|
if (i % 20 == 0)
|
||||||
|
{
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="CornerClutterDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(CornerClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.SpawnGroups.TryGetValue(DungeonDataKey.CornerClutter, out var corner))
|
||||||
|
{
|
||||||
|
_sawmill.Error(Environment.StackTrace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tile in dungeon.CorridorTiles)
|
||||||
|
{
|
||||||
|
var blocked = _anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask);
|
||||||
|
|
||||||
|
if (blocked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If at least 2 adjacent tiles are blocked consider it a corner
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) (i * 2);
|
||||||
|
blocked = HasWall(tile + dir.ToIntVec());
|
||||||
|
|
||||||
|
if (!blocked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var nextDir = (Direction) ((i + 1) * 2 % 8);
|
||||||
|
blocked = HasWall(tile + nextDir.ToIntVec());
|
||||||
|
|
||||||
|
if (!blocked)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (random.Prob(gen.Chance))
|
||||||
|
{
|
||||||
|
var coords = _maps.GridTileToLocal(_gridUid, _grid, tile);
|
||||||
|
var protos = EntitySpawnCollection.GetSpawns(_prototype.Index(corner).Entries, random);
|
||||||
|
_entManager.SpawnEntities(coords, protos);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="CorridorDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(CorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(CorridorDunGen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entrances = new List<Vector2i>(dungeon.Rooms.Count);
|
||||||
|
|
||||||
|
// Grab entrances
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
entrances.AddRange(room.Entrances);
|
||||||
|
}
|
||||||
|
|
||||||
|
var edges = _dungeon.MinimumSpanningTree(entrances, random);
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: Add in say 1/3 of edges back in to add some cyclic to it.
|
||||||
|
|
||||||
|
var expansion = gen.Width - 2;
|
||||||
|
// Okay so tl;dr is that we don't want to cut close to rooms as it might go from 3 width to 2 width suddenly
|
||||||
|
// So we will add a buffer range around each room to deter pathfinding there unless necessary
|
||||||
|
var deterredTiles = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
if (expansion >= 1)
|
||||||
|
{
|
||||||
|
foreach (var tile in dungeon.RoomExteriorTiles)
|
||||||
|
{
|
||||||
|
for (var x = -expansion; x <= expansion; x++)
|
||||||
|
{
|
||||||
|
for (var y = -expansion; y <= expansion; y++)
|
||||||
|
{
|
||||||
|
var neighbor = new Vector2(tile.X + x, tile.Y + y).Floored();
|
||||||
|
|
||||||
|
if (dungeon.RoomTiles.Contains(neighbor) ||
|
||||||
|
dungeon.RoomExteriorTiles.Contains(neighbor) ||
|
||||||
|
entrances.Contains(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
deterredTiles.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
foreach (var entrance in room.Entrances)
|
||||||
|
{
|
||||||
|
// Just so we can still actually get in to the entrance we won't deter from a tile away from it.
|
||||||
|
var normal = (entrance + _grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
|
||||||
|
deterredTiles.Remove(entrance + normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var excludedTiles = new HashSet<Vector2i>(dungeon.RoomExteriorTiles);
|
||||||
|
excludedTiles.UnionWith(dungeon.RoomTiles);
|
||||||
|
var corridorTiles = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
_dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile =>
|
||||||
|
{
|
||||||
|
var mod = 1f;
|
||||||
|
|
||||||
|
if (corridorTiles.Contains(tile))
|
||||||
|
{
|
||||||
|
mod *= 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deterredTiles.Contains(tile))
|
||||||
|
{
|
||||||
|
mod *= 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
});
|
||||||
|
|
||||||
|
WidenCorridor(dungeon, gen.Width, corridorTiles);
|
||||||
|
|
||||||
|
var setTiles = new List<(Vector2i, Tile)>();
|
||||||
|
var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
|
||||||
|
|
||||||
|
foreach (var tile in corridorTiles)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(tile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, setTiles);
|
||||||
|
dungeon.CorridorTiles.UnionWith(corridorTiles);
|
||||||
|
dungeon.RefreshAllTiles();
|
||||||
|
BuildCorridorExterior(dungeon);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,17 @@ using System.Threading.Tasks;
|
|||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
public sealed partial class DungeonJob
|
public sealed partial class DungeonJob
|
||||||
{
|
{
|
||||||
private async Task PostGen(CorridorClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
|
/// <summary>
|
||||||
Random random)
|
/// <see cref="CorridorClutterDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(CorridorClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
{
|
{
|
||||||
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||||
var count = (int) Math.Ceiling(dungeon.CorridorTiles.Count * gen.Chance);
|
var count = (int) Math.Ceiling(dungeon.CorridorTiles.Count * gen.Chance);
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Doors.Components;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="CorridorDecalSkirtingDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(CorridorDecalSkirtingDunGen decks, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Colors.TryGetValue(DungeonDataKey.Decals, out var color))
|
||||||
|
{
|
||||||
|
_sawmill.Error(Environment.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
var directions = new ValueList<DirectionFlag>(4);
|
||||||
|
var pocketDirections = new ValueList<Direction>(4);
|
||||||
|
var doorQuery = _entManager.GetEntityQuery<DoorComponent>();
|
||||||
|
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||||
|
var offset = -_grid.TileSizeHalfVector;
|
||||||
|
|
||||||
|
foreach (var tile in dungeon.CorridorTiles)
|
||||||
|
{
|
||||||
|
DebugTools.Assert(!dungeon.RoomTiles.Contains(tile));
|
||||||
|
directions.Clear();
|
||||||
|
|
||||||
|
// Do cardinals 1 step
|
||||||
|
// Do corners the other step
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var dir = (DirectionFlag) Math.Pow(2, i);
|
||||||
|
var neighbor = tile + dir.AsDir().ToIntVec();
|
||||||
|
|
||||||
|
var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
|
||||||
|
|
||||||
|
while (anc.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
if (!physicsQuery.TryGetComponent(ent, out var physics) ||
|
||||||
|
!physics.CanCollide ||
|
||||||
|
!physics.Hard ||
|
||||||
|
doorQuery.HasComponent(ent.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
directions.Add(dir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pockets
|
||||||
|
if (directions.Count == 0)
|
||||||
|
{
|
||||||
|
pocketDirections.Clear();
|
||||||
|
|
||||||
|
for (var i = 1; i < 5; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) (i * 2 - 1);
|
||||||
|
var neighbor = tile + dir.ToIntVec();
|
||||||
|
|
||||||
|
var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
|
||||||
|
|
||||||
|
while (anc.MoveNext(out var ent))
|
||||||
|
{
|
||||||
|
if (!physicsQuery.TryGetComponent(ent, out var physics) ||
|
||||||
|
!physics.CanCollide ||
|
||||||
|
!physics.Hard ||
|
||||||
|
doorQuery.HasComponent(ent.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pocketDirections.Add(dir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pocketDirections.Count == 1)
|
||||||
|
{
|
||||||
|
if (decks.PocketDecals.TryGetValue(pocketDirections[0], out var cDir))
|
||||||
|
{
|
||||||
|
// Decals not being centered biting my ass again
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
|
||||||
|
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directions.Count == 1)
|
||||||
|
{
|
||||||
|
if (decks.CardinalDecals.TryGetValue(directions[0], out var cDir))
|
||||||
|
{
|
||||||
|
// Decals not being centered biting my ass again
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
|
||||||
|
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
if (directions.Count == 2)
|
||||||
|
{
|
||||||
|
// Auehghegueugegegeheh help me
|
||||||
|
var dirFlag = directions[0] | directions[1];
|
||||||
|
|
||||||
|
if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
|
||||||
|
{
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
|
||||||
|
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="DungeonEntranceDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(DungeonEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entrance))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(DungeonEntranceDunGen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rooms = new List<DungeonRoom>(dungeon.Rooms);
|
||||||
|
var roomTiles = new List<Vector2i>();
|
||||||
|
var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
|
||||||
|
|
||||||
|
for (var i = 0; i < gen.Count; i++)
|
||||||
|
{
|
||||||
|
var roomIndex = random.Next(rooms.Count);
|
||||||
|
var room = rooms[roomIndex];
|
||||||
|
|
||||||
|
// Move out 3 tiles in a direction away from center of the room
|
||||||
|
// If none of those intersect another tile it's probably external
|
||||||
|
// TODO: Maybe need to take top half of furthest rooms in case there's interior exits?
|
||||||
|
roomTiles.AddRange(room.Exterior);
|
||||||
|
random.Shuffle(roomTiles);
|
||||||
|
|
||||||
|
foreach (var tile in roomTiles)
|
||||||
|
{
|
||||||
|
var isValid = false;
|
||||||
|
|
||||||
|
// Check if one side is dungeon and the other side is nothing.
|
||||||
|
for (var j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) (j * 2);
|
||||||
|
var oppositeDir = dir.GetOpposite();
|
||||||
|
var dirVec = tile + dir.ToIntVec();
|
||||||
|
var oppositeDirVec = tile + oppositeDir.ToIntVec();
|
||||||
|
|
||||||
|
if (!dungeon.RoomTiles.Contains(dirVec))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dungeon.RoomTiles.Contains(oppositeDirVec) ||
|
||||||
|
dungeon.RoomExteriorTiles.Contains(oppositeDirVec) ||
|
||||||
|
dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) ||
|
||||||
|
dungeon.CorridorTiles.Contains(oppositeDirVec))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if exterior spot free.
|
||||||
|
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if interior spot free (no guarantees on exterior but ClearDoor should handle it)
|
||||||
|
if (!_anchorable.TileFree(_grid, dirVec, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid pick!
|
||||||
|
isValid = true;
|
||||||
|
|
||||||
|
// Entrance wew
|
||||||
|
_maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile(tileDef, random));
|
||||||
|
ClearDoor(dungeon, _grid, tile);
|
||||||
|
var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile);
|
||||||
|
// Need to offset the spawn to avoid spawning in the room.
|
||||||
|
|
||||||
|
foreach (var ent in EntitySpawnCollection.GetSpawns(_prototype.Index(entrance).Entries, random))
|
||||||
|
{
|
||||||
|
_entManager.SpawnAtPosition(ent, gridCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear out any biome tiles nearby to avoid blocking it
|
||||||
|
foreach (var nearTile in _maps.GetLocalTilesIntersecting(_gridUid, _grid, new Circle(gridCoords.Position, 1.5f), false))
|
||||||
|
{
|
||||||
|
if (dungeon.RoomTiles.Contains(nearTile.GridIndices) ||
|
||||||
|
dungeon.RoomExteriorTiles.Contains(nearTile.GridIndices) ||
|
||||||
|
dungeon.CorridorTiles.Contains(nearTile.GridIndices) ||
|
||||||
|
dungeon.CorridorExteriorTiles.Contains(nearTile.GridIndices))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTile(_gridUid, _grid, nearTile.GridIndices, _tile.GetVariantTile(tileDef, random));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
roomTiles.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="EntranceFlankDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(EntranceFlankDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tiles = new List<(Vector2i Index, Tile)>();
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
var spawnPositions = new ValueList<Vector2i>(dungeon.Rooms.Count);
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
foreach (var entrance in room.Entrances)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) i;
|
||||||
|
var neighbor = entrance + dir.ToIntVec();
|
||||||
|
|
||||||
|
if (!dungeon.RoomExteriorTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
|
||||||
|
spawnPositions.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
var entGroup = _prototype.Index(flankProto);
|
||||||
|
|
||||||
|
foreach (var entrance in spawnPositions)
|
||||||
|
{
|
||||||
|
_entManager.SpawnEntities(_maps.GridTileToLocal(_gridUid, _grid, entrance), EntitySpawnCollection.GetSpawns(entGroup.Entries, random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
// (Comment refers to internal & external).
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may be wondering why these are different.
|
||||||
|
* It's because for internals we want to force it as it looks nicer and not leave it up to chance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Can probably combine these a bit, their differences are in really annoying to pull out spots.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ExternalWindowDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(ExternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate every tile with N chance to spawn windows on that wall per cardinal dir.
|
||||||
|
var chance = 0.25 / 3f;
|
||||||
|
|
||||||
|
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
|
||||||
|
allExterior.UnionWith(dungeon.RoomExteriorTiles);
|
||||||
|
var validTiles = allExterior.ToList();
|
||||||
|
random.Shuffle(validTiles);
|
||||||
|
|
||||||
|
var tiles = new List<(Vector2i, Tile)>();
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
var count = Math.Floor(validTiles.Count * chance);
|
||||||
|
var index = 0;
|
||||||
|
var takenTiles = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
// There's a bunch of shit here but tl;dr
|
||||||
|
// - don't spawn over cap
|
||||||
|
// - Check if we have 3 tiles in a row that aren't corners and aren't obstructed
|
||||||
|
foreach (var tile in validTiles)
|
||||||
|
{
|
||||||
|
if (index > count)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Room tile / already used.
|
||||||
|
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask) ||
|
||||||
|
takenTiles.Contains(tile))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we're not on a corner
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) (i * 2);
|
||||||
|
var dirVec = dir.ToIntVec();
|
||||||
|
var isValid = true;
|
||||||
|
|
||||||
|
// Check 1 beyond either side to ensure it's not a corner.
|
||||||
|
for (var j = -1; j < 4; j++)
|
||||||
|
{
|
||||||
|
var neighbor = tile + dirVec * j;
|
||||||
|
|
||||||
|
if (!allExterior.Contains(neighbor) ||
|
||||||
|
takenTiles.Contains(neighbor) ||
|
||||||
|
!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check perpendicular that it is free
|
||||||
|
foreach (var k in new [] {2, 6})
|
||||||
|
{
|
||||||
|
var perp = (Direction) ((i * 2 + k) % 8);
|
||||||
|
var perpVec = perp.ToIntVec();
|
||||||
|
var perpTile = tile + perpVec;
|
||||||
|
|
||||||
|
if (allExterior.Contains(perpTile) ||
|
||||||
|
takenTiles.Contains(neighbor) ||
|
||||||
|
!_anchorable.TileFree(_grid, perpTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var j = 0; j < 3; j++)
|
||||||
|
{
|
||||||
|
var neighbor = tile + dirVec * j;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
|
||||||
|
index++;
|
||||||
|
takenTiles.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
index = 0;
|
||||||
|
var spawnEntry = _prototype.Index(windowGroup);
|
||||||
|
|
||||||
|
foreach (var tile in tiles)
|
||||||
|
{
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1);
|
||||||
|
|
||||||
|
index += spawnEntry.Entries.Count;
|
||||||
|
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(spawnEntry.Entries, random));
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="InternalWindowDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(InternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Unable to find dungeon data keys for {nameof(gen)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate every room and check if there's a gap beyond it that leads to another room within N tiles
|
||||||
|
// If so then consider windows
|
||||||
|
var minDistance = 4;
|
||||||
|
var maxDistance = 6;
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
var window = _prototype.Index(windowGroup);
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
var validTiles = new List<Vector2i>();
|
||||||
|
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var dir = (DirectionFlag) Math.Pow(2, i);
|
||||||
|
var dirVec = dir.AsDir().ToIntVec();
|
||||||
|
|
||||||
|
foreach (var tile in room.Tiles)
|
||||||
|
{
|
||||||
|
var tileAngle = (tile + _grid.TileSizeHalfVector - room.Center).ToAngle();
|
||||||
|
var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2);
|
||||||
|
|
||||||
|
var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded();
|
||||||
|
|
||||||
|
if (!tileVec.Equals(dirVec))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var valid = false;
|
||||||
|
|
||||||
|
for (var j = 1; j < maxDistance; j++)
|
||||||
|
{
|
||||||
|
var edgeNeighbor = tile + dirVec * j;
|
||||||
|
|
||||||
|
if (dungeon.RoomTiles.Contains(edgeNeighbor))
|
||||||
|
{
|
||||||
|
if (j < minDistance)
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var windowTile = tile + dirVec;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(windowTile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, windowTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
validTiles.Add(windowTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
validTiles.Sort((x, y) => (x + _grid.TileSizeHalfVector - room.Center).LengthSquared().CompareTo((y + _grid.TileSizeHalfVector - room.Center).LengthSquared()));
|
||||||
|
|
||||||
|
for (var j = 0; j < Math.Min(validTiles.Count, 3); j++)
|
||||||
|
{
|
||||||
|
var tile = validTiles[j];
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
|
||||||
|
_maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
|
||||||
|
|
||||||
|
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(window.Entries, random));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validTiles.Count > 0)
|
||||||
|
{
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validTiles.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="JunctionDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(JunctionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Junction, out var junctionProto))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Dungeon data keys are missing for {nameof(gen)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
var entranceGroup = _prototype.Index(junctionProto);
|
||||||
|
|
||||||
|
// N-wide junctions
|
||||||
|
foreach (var tile in dungeon.CorridorTiles)
|
||||||
|
{
|
||||||
|
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check each direction:
|
||||||
|
// - Check if immediate neighbors are free
|
||||||
|
// - Check if the neighbors beyond that are not free
|
||||||
|
// - Then check either side if they're slightly more free
|
||||||
|
var exteriorWidth = (int) Math.Floor(gen.Width / 2f);
|
||||||
|
var width = (int) Math.Ceiling(gen.Width / 2f);
|
||||||
|
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
var isValid = true;
|
||||||
|
var neighborDir = (Direction) (i * 2);
|
||||||
|
var neighborVec = neighborDir.ToIntVec();
|
||||||
|
|
||||||
|
for (var j = -width; j <= width; j++)
|
||||||
|
{
|
||||||
|
if (j == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = tile + neighborVec * j;
|
||||||
|
|
||||||
|
// If it's an end tile then check it's occupied.
|
||||||
|
if (j == -width ||
|
||||||
|
j == width)
|
||||||
|
{
|
||||||
|
if (!HasWall(neighbor))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not at the end tile then check it + perpendicular are free.
|
||||||
|
if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var perp1 = tile + neighborVec * j + ((Direction) ((i * 2 + 2) % 8)).ToIntVec();
|
||||||
|
var perp2 = tile + neighborVec * j + ((Direction) ((i * 2 + 6) % 8)).ToIntVec();
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, perp1, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, perp2, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check corners to see if either side opens up (if it's just a 1x wide corridor do nothing, needs to be a funnel.
|
||||||
|
foreach (var j in new [] {-exteriorWidth, exteriorWidth})
|
||||||
|
{
|
||||||
|
var freeCount = 0;
|
||||||
|
|
||||||
|
// Need at least 3 of 4 free
|
||||||
|
for (var k = 0; k < 4; k++)
|
||||||
|
{
|
||||||
|
var cornerDir = (Direction) (k * 2 + 1);
|
||||||
|
var cornerVec = cornerDir.ToIntVec();
|
||||||
|
var cornerNeighbor = tile + neighborVec * j + cornerVec;
|
||||||
|
|
||||||
|
if (_anchorable.TileFree(_grid, cornerNeighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
{
|
||||||
|
freeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeCount < gen.Width)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Valid!
|
||||||
|
isValid = true;
|
||||||
|
|
||||||
|
for (var x = -width + 1; x < width; x++)
|
||||||
|
{
|
||||||
|
var weh = tile + neighborDir.ToIntVec() * x;
|
||||||
|
|
||||||
|
if (reservedTiles.Contains(weh))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_maps.SetTile(_gridUid, _grid, weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
|
||||||
|
|
||||||
|
var coords = _maps.GridTileToLocal(_gridUid, _grid, weh);
|
||||||
|
_entManager.SpawnEntities(coords, EntitySpawnCollection.GetSpawns(entranceGroup.Entries, random));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="MiddleConnectionDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(MiddleConnectionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProto) ||
|
||||||
|
!_prototype.TryIndex(entranceProto, out var entrance))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Tried to run {nameof(MiddleConnectionDunGen)} without any dungeon data set which is unsupported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto);
|
||||||
|
_prototype.TryIndex(flankProto, out var flank);
|
||||||
|
|
||||||
|
// Grab all of the room bounds
|
||||||
|
// Then, work out connections between them
|
||||||
|
var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
var roomEdges = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
foreach (var index in room.Tiles)
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
// Cardinals only
|
||||||
|
if (x != 0 && y != 0 ||
|
||||||
|
x == 0 && y == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neighbor = new Vector2i(index.X + x, index.Y + y);
|
||||||
|
|
||||||
|
if (dungeon.RoomTiles.Contains(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
roomEdges.Add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roomBorders.Add(room, roomEdges);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do pathfind from first room to work out graph.
|
||||||
|
// TODO: Optional loops
|
||||||
|
|
||||||
|
var roomConnections = new Dictionary<DungeonRoom, List<DungeonRoom>>();
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
|
||||||
|
foreach (var (room, border) in roomBorders)
|
||||||
|
{
|
||||||
|
var conns = roomConnections.GetOrNew(room);
|
||||||
|
|
||||||
|
foreach (var (otherRoom, otherBorders) in roomBorders)
|
||||||
|
{
|
||||||
|
if (room.Equals(otherRoom) ||
|
||||||
|
conns.Contains(otherRoom))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flipp = new HashSet<Vector2i>(border);
|
||||||
|
flipp.IntersectWith(otherBorders);
|
||||||
|
|
||||||
|
if (flipp.Count == 0 ||
|
||||||
|
gen.OverlapCount != -1 && flipp.Count != gen.OverlapCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var center = Vector2.Zero;
|
||||||
|
|
||||||
|
foreach (var node in flipp)
|
||||||
|
{
|
||||||
|
center += node + _grid.TileSizeHalfVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
center /= flipp.Count;
|
||||||
|
// Weight airlocks towards center more.
|
||||||
|
var nodeDistances = new List<(Vector2i Node, float Distance)>(flipp.Count);
|
||||||
|
|
||||||
|
foreach (var node in flipp)
|
||||||
|
{
|
||||||
|
nodeDistances.Add((node, (node + _grid.TileSizeHalfVector - center).LengthSquared()));
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeDistances.Sort((x, y) => x.Distance.CompareTo(y.Distance));
|
||||||
|
|
||||||
|
var width = gen.Count;
|
||||||
|
|
||||||
|
for (var i = 0; i < nodeDistances.Count; i++)
|
||||||
|
{
|
||||||
|
var node = nodeDistances[i].Node;
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, node);
|
||||||
|
if (!_anchorable.TileFree(_grid, node, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
width--;
|
||||||
|
_maps.SetTile(_gridUid, _grid, node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
|
||||||
|
|
||||||
|
if (flank != null && nodeDistances.Count - i <= 2)
|
||||||
|
{
|
||||||
|
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(flank.Entries, random));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Iterate neighbors and check for blockers, if so bulldoze
|
||||||
|
ClearDoor(dungeon, _grid, node);
|
||||||
|
|
||||||
|
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(entrance.Entries, random));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
conns.Add(otherRoom);
|
||||||
|
var otherConns = roomConnections.GetOrNew(otherRoom);
|
||||||
|
otherConns.Add(room);
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="RoomEntranceDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(RoomEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
|
||||||
|
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProtos) ||
|
||||||
|
!_prototype.TryIndex(entranceProtos, out var entranceIn))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(RoomEntranceDunGen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var setTiles = new List<(Vector2i, Tile)>();
|
||||||
|
var tileDef = _tileDefManager[tileProto];
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
foreach (var entrance in room.Entrances)
|
||||||
|
{
|
||||||
|
setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, setTiles);
|
||||||
|
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
foreach (var entrance in room.Entrances)
|
||||||
|
{
|
||||||
|
_entManager.SpawnEntities(
|
||||||
|
_maps.GridTileToLocal(_gridUid, _grid, entrance),
|
||||||
|
EntitySpawnCollection.GetSpawns(entranceIn.Entries, random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.NPC.Pathfinding;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="SplineDungeonConnectorDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<Dungeon> PostGen(
|
||||||
|
SplineDungeonConnectorDunGen gen,
|
||||||
|
DungeonData data,
|
||||||
|
List<Dungeon> dungeons,
|
||||||
|
HashSet<Vector2i> reservedTiles,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
// TODO: The path itself use the tile
|
||||||
|
// Widen it randomly (probably for each tile offset it by some changing amount).
|
||||||
|
|
||||||
|
// NOOP
|
||||||
|
if (dungeons.Count <= 1)
|
||||||
|
return Dungeon.Empty;
|
||||||
|
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var fallback) ||
|
||||||
|
!data.Tiles.TryGetValue(DungeonDataKey.WidenTile, out var widen))
|
||||||
|
{
|
||||||
|
LogDataError(typeof(SplineDungeonConnectorDunGen));
|
||||||
|
return Dungeon.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes = new List<Vector2i>();
|
||||||
|
|
||||||
|
foreach (var dungeon in dungeons)
|
||||||
|
{
|
||||||
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
if (room.Entrances.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
nodes.Add(room.Entrances[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tree = _dungeon.MinimumSpanningTree(nodes, random);
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return Dungeon.Empty;
|
||||||
|
|
||||||
|
var tiles = new List<(Vector2i Index, Tile Tile)>();
|
||||||
|
var pathfinding = _entManager.System<PathfindingSystem>();
|
||||||
|
var allTiles = new HashSet<Vector2i>();
|
||||||
|
var fallbackTile = new Tile(_prototype.Index(fallback).TileId);
|
||||||
|
|
||||||
|
foreach (var pair in tree)
|
||||||
|
{
|
||||||
|
var path = pathfinding.GetSplinePath(new PathfindingSystem.SplinePathArgs()
|
||||||
|
{
|
||||||
|
Distance = gen.DivisionDistance,
|
||||||
|
MaxRatio = gen.VarianceMax,
|
||||||
|
Args = new PathfindingSystem.SimplePathArgs()
|
||||||
|
{
|
||||||
|
Start = pair.Start,
|
||||||
|
End = pair.End,
|
||||||
|
TileCost = node =>
|
||||||
|
{
|
||||||
|
// We want these to get prioritised internally and into space if it's a space dungeon.
|
||||||
|
if (_maps.TryGetTile(_grid, node, out var tile) && !tile.IsEmpty)
|
||||||
|
return 1f;
|
||||||
|
|
||||||
|
return 5f;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
random);
|
||||||
|
|
||||||
|
// Welp
|
||||||
|
if (path.Path.Count == 0)
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Unable to connect spline dungeon path for {_entManager.ToPrettyString(_gridUid)} between {pair.Start} and {pair.End}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return Dungeon.Empty;
|
||||||
|
|
||||||
|
var wide = pathfinding.GetWiden(new PathfindingSystem.WidenArgs()
|
||||||
|
{
|
||||||
|
Path = path.Path,
|
||||||
|
},
|
||||||
|
random);
|
||||||
|
|
||||||
|
tiles.Clear();
|
||||||
|
allTiles.EnsureCapacity(allTiles.Count + wide.Count);
|
||||||
|
|
||||||
|
foreach (var node in wide)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
allTiles.Add(node);
|
||||||
|
Tile tile;
|
||||||
|
|
||||||
|
if (random.Prob(0.9f))
|
||||||
|
{
|
||||||
|
tile = new Tile(_prototype.Index(widen).TileId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tile = _tileDefManager.GetVariantTile(widen, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.Add((node, tile));
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
tiles.Clear();
|
||||||
|
allTiles.EnsureCapacity(allTiles.Count + path.Path.Count);
|
||||||
|
|
||||||
|
foreach (var node in path.Path)
|
||||||
|
{
|
||||||
|
if (reservedTiles.Contains(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
allTiles.Add(node);
|
||||||
|
tiles.Add((node, fallbackTile));
|
||||||
|
}
|
||||||
|
|
||||||
|
_maps.SetTiles(_gridUid, _grid, tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dungy = new Dungeon();
|
||||||
|
var dungyRoom = new DungeonRoom(allTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
|
||||||
|
dungy.AddRoom(dungyRoom);
|
||||||
|
|
||||||
|
return dungy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="WallMountDunGen"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task PostGen(WallMountDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Tried to run {nameof(WallMountDunGen)} without any dungeon data set which is unsupported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileDef = _prototype.Index(tileProto);
|
||||||
|
data.SpawnGroups.TryGetValue(DungeonDataKey.WallMounts, out var spawnProto);
|
||||||
|
|
||||||
|
var checkedTiles = new HashSet<Vector2i>();
|
||||||
|
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
|
||||||
|
allExterior.UnionWith(dungeon.RoomExteriorTiles);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
foreach (var neighbor in allExterior)
|
||||||
|
{
|
||||||
|
// Occupado
|
||||||
|
if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_maps.SetTile(_gridUid, _grid, neighbor, _tile.GetVariantTile(tileDef, random));
|
||||||
|
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor);
|
||||||
|
var protoNames = EntitySpawnCollection.GetSpawns(_prototype.Index(spawnProto).Entries, random);
|
||||||
|
|
||||||
|
_entManager.SpawnEntities(gridPos, protoNames);
|
||||||
|
count += protoNames.Count;
|
||||||
|
|
||||||
|
if (count > 20)
|
||||||
|
{
|
||||||
|
count -= 20;
|
||||||
|
await SuspendDungeon();
|
||||||
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
using Robust.Shared.Collections;
|
using Robust.Shared.Collections;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
public sealed partial class DungeonJob
|
public sealed partial class DungeonJob
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to connect rooms via worm-like corridors.
|
/// <see cref="WormCorridorDunGen"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
|
private async Task PostGen(WormCorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
|
||||||
{
|
{
|
||||||
|
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) || !_prototype.TryIndex(tileProto, out var tileDef))
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Tried to run {nameof(WormCorridorDunGen)} without any dungeon data set which is unsupported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var networks = new List<(Vector2i Start, HashSet<Vector2i> Network)>();
|
var networks = new List<(Vector2i Start, HashSet<Vector2i> Network)>();
|
||||||
|
|
||||||
// List of places to start from.
|
// List of places to start from.
|
||||||
@@ -32,7 +36,7 @@ public sealed partial class DungeonJob
|
|||||||
networks.Add((entrance, network));
|
networks.Add((entrance, network));
|
||||||
|
|
||||||
// Point away from the room to start with.
|
// Point away from the room to start with.
|
||||||
startAngles.Add(entrance, (entrance + grid.TileSizeHalfVector - room.Center).ToAngle());
|
startAngles.Add(entrance, (entrance + _grid.TileSizeHalfVector - room.Center).ToAngle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +50,7 @@ public sealed partial class DungeonJob
|
|||||||
// Find a random network to worm from.
|
// Find a random network to worm from.
|
||||||
var startIndex = (i % networks.Count);
|
var startIndex = (i % networks.Count);
|
||||||
var startPos = networks[startIndex].Start;
|
var startPos = networks[startIndex].Start;
|
||||||
var position = startPos + grid.TileSizeHalfVector;
|
var position = startPos + _grid.TileSizeHalfVector;
|
||||||
|
|
||||||
var remainingLength = gen.Length;
|
var remainingLength = gen.Length;
|
||||||
worm.Clear();
|
worm.Clear();
|
||||||
@@ -108,7 +112,7 @@ public sealed partial class DungeonJob
|
|||||||
costSoFar[startNode] = 0f;
|
costSoFar[startNode] = 0f;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
await SuspendDungeon();
|
||||||
if (!ValidateResume())
|
if (!ValidateResume())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -174,9 +178,9 @@ public sealed partial class DungeonJob
|
|||||||
WidenCorridor(dungeon, gen.Width, main.Network);
|
WidenCorridor(dungeon, gen.Width, main.Network);
|
||||||
dungeon.CorridorTiles.UnionWith(main.Network);
|
dungeon.CorridorTiles.UnionWith(main.Network);
|
||||||
BuildCorridorExterior(dungeon);
|
BuildCorridorExterior(dungeon);
|
||||||
|
dungeon.RefreshAllTiles();
|
||||||
|
|
||||||
var tiles = new List<(Vector2i Index, Tile Tile)>();
|
var tiles = new List<(Vector2i Index, Tile Tile)>();
|
||||||
var tileDef = _prototype.Index(gen.Tile);
|
|
||||||
|
|
||||||
foreach (var tile in dungeon.CorridorTiles)
|
foreach (var tile in dungeon.CorridorTiles)
|
||||||
{
|
{
|
||||||
309
Content.Server/Procedural/DungeonJob/DungeonJob.cs
Normal file
309
Content.Server/Procedural/DungeonJob/DungeonJob.cs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Decals;
|
||||||
|
using Content.Server.NPC.Components;
|
||||||
|
using Content.Server.NPC.HTN;
|
||||||
|
using Content.Server.NPC.Systems;
|
||||||
|
using Content.Shared.Construction.EntitySystems;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
using Content.Shared.Procedural.DungeonLayers;
|
||||||
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Tag;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.Physics;
|
||||||
|
using Robust.Shared.CPUJob.JobQueues;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using IDunGenLayer = Content.Shared.Procedural.IDunGenLayer;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural.DungeonJob;
|
||||||
|
|
||||||
|
public sealed partial class DungeonJob : Job<List<Dungeon>>
|
||||||
|
{
|
||||||
|
public bool TimeSlice = true;
|
||||||
|
|
||||||
|
private readonly IEntityManager _entManager;
|
||||||
|
private readonly IPrototypeManager _prototype;
|
||||||
|
private readonly ITileDefinitionManager _tileDefManager;
|
||||||
|
|
||||||
|
private readonly AnchorableSystem _anchorable;
|
||||||
|
private readonly DecalSystem _decals;
|
||||||
|
private readonly DungeonSystem _dungeon;
|
||||||
|
private readonly EntityLookupSystem _lookup;
|
||||||
|
private readonly TagSystem _tags;
|
||||||
|
private readonly TileSystem _tile;
|
||||||
|
private readonly SharedMapSystem _maps;
|
||||||
|
private readonly SharedTransformSystem _transform;
|
||||||
|
|
||||||
|
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||||
|
private EntityQuery<TransformComponent> _xformQuery;
|
||||||
|
|
||||||
|
private readonly DungeonConfigPrototype _gen;
|
||||||
|
private readonly int _seed;
|
||||||
|
private readonly Vector2i _position;
|
||||||
|
|
||||||
|
private readonly EntityUid _gridUid;
|
||||||
|
private readonly MapGridComponent _grid;
|
||||||
|
|
||||||
|
private readonly ISawmill _sawmill;
|
||||||
|
|
||||||
|
public DungeonJob(
|
||||||
|
ISawmill sawmill,
|
||||||
|
double maxTime,
|
||||||
|
IEntityManager entManager,
|
||||||
|
IPrototypeManager prototype,
|
||||||
|
ITileDefinitionManager tileDefManager,
|
||||||
|
AnchorableSystem anchorable,
|
||||||
|
DecalSystem decals,
|
||||||
|
DungeonSystem dungeon,
|
||||||
|
EntityLookupSystem lookup,
|
||||||
|
TileSystem tile,
|
||||||
|
SharedTransformSystem transform,
|
||||||
|
DungeonConfigPrototype gen,
|
||||||
|
MapGridComponent grid,
|
||||||
|
EntityUid gridUid,
|
||||||
|
int seed,
|
||||||
|
Vector2i position,
|
||||||
|
CancellationToken cancellation = default) : base(maxTime, cancellation)
|
||||||
|
{
|
||||||
|
_sawmill = sawmill;
|
||||||
|
_entManager = entManager;
|
||||||
|
_prototype = prototype;
|
||||||
|
_tileDefManager = tileDefManager;
|
||||||
|
|
||||||
|
_anchorable = anchorable;
|
||||||
|
_decals = decals;
|
||||||
|
_dungeon = dungeon;
|
||||||
|
_lookup = lookup;
|
||||||
|
_tile = tile;
|
||||||
|
_tags = _entManager.System<TagSystem>();
|
||||||
|
_maps = _entManager.System<SharedMapSystem>();
|
||||||
|
_transform = transform;
|
||||||
|
|
||||||
|
_physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||||
|
_xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
|
_gen = gen;
|
||||||
|
_grid = grid;
|
||||||
|
_gridUid = gridUid;
|
||||||
|
_seed = seed;
|
||||||
|
_position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relevant dungeon, running recursively as relevant.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
|
||||||
|
private async Task<List<Dungeon>> GetDungeons(
|
||||||
|
Vector2i position,
|
||||||
|
DungeonConfigPrototype config,
|
||||||
|
DungeonData data,
|
||||||
|
List<IDunGenLayer> layers,
|
||||||
|
HashSet<Vector2i> reservedTiles,
|
||||||
|
int seed,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
var dungeons = new List<Dungeon>();
|
||||||
|
var count = random.Next(config.MinCount, config.MaxCount + 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
position += random.NextPolarVector2(config.MinOffset, config.MaxOffset).Floored();
|
||||||
|
|
||||||
|
foreach (var layer in layers)
|
||||||
|
{
|
||||||
|
await RunLayer(dungeons, data, position, layer, reservedTiles, seed, random);
|
||||||
|
|
||||||
|
if (config.ReserveTiles)
|
||||||
|
{
|
||||||
|
foreach (var dungeon in dungeons)
|
||||||
|
{
|
||||||
|
reservedTiles.UnionWith(dungeon.AllTiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SuspendDungeon();
|
||||||
|
if (!ValidateResume())
|
||||||
|
return new List<Dungeon>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dungeons;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<List<Dungeon>?> Process()
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
|
||||||
|
_grid.CanSplit = false;
|
||||||
|
var random = new Random(_seed);
|
||||||
|
var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored();
|
||||||
|
|
||||||
|
// Tiles we can no longer generate on due to being reserved elsewhere.
|
||||||
|
var reservedTiles = new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
var dungeons = await GetDungeons(position, _gen, _gen.Data, _gen.Layers, reservedTiles, _seed, random);
|
||||||
|
// To make it slightly more deterministic treat this RNG as separate ig.
|
||||||
|
|
||||||
|
// Post-processing after finishing loading.
|
||||||
|
|
||||||
|
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
|
||||||
|
_grid.CanSplit = true;
|
||||||
|
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
|
||||||
|
var npcSystem = _entManager.System<NPCSystem>();
|
||||||
|
var npcs = new HashSet<Entity<HTNComponent>>();
|
||||||
|
|
||||||
|
_lookup.GetChildEntities(_gridUid, npcs);
|
||||||
|
|
||||||
|
foreach (var npc in npcs)
|
||||||
|
{
|
||||||
|
npcSystem.WakeNPC(npc.Owner, npc.Comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dungeons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunLayer(
|
||||||
|
List<Dungeon> dungeons,
|
||||||
|
DungeonData data,
|
||||||
|
Vector2i position,
|
||||||
|
IDunGenLayer layer,
|
||||||
|
HashSet<Vector2i> reservedTiles,
|
||||||
|
int seed,
|
||||||
|
Random random)
|
||||||
|
{
|
||||||
|
_sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen.ID} with seed {_seed}");
|
||||||
|
|
||||||
|
// If there's a way to just call the methods directly for the love of god tell me.
|
||||||
|
// Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should
|
||||||
|
// never be reserved)
|
||||||
|
|
||||||
|
// Some may or may not return dungeons.
|
||||||
|
// It's clamplicated but yeah procgen layering moment I'll take constructive feedback.
|
||||||
|
|
||||||
|
switch (layer)
|
||||||
|
{
|
||||||
|
case AutoCablingDunGen cabling:
|
||||||
|
await PostGen(cabling, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case BiomeMarkerLayerDunGen markerPost:
|
||||||
|
await PostGen(markerPost, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case BiomeDunGen biome:
|
||||||
|
await PostGen(biome, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case BoundaryWallDunGen boundary:
|
||||||
|
await PostGen(boundary, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case CornerClutterDunGen clutter:
|
||||||
|
await PostGen(clutter, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case CorridorClutterDunGen corClutter:
|
||||||
|
await PostGen(corClutter, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case CorridorDunGen cordor:
|
||||||
|
await PostGen(cordor, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case CorridorDecalSkirtingDunGen decks:
|
||||||
|
await PostGen(decks, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case EntranceFlankDunGen flank:
|
||||||
|
await PostGen(flank, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case ExteriorDunGen exterior:
|
||||||
|
dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
|
||||||
|
break;
|
||||||
|
case FillGridDunGen fill:
|
||||||
|
dungeons.Add(await GenerateFillDunGen(data, reservedTiles));
|
||||||
|
break;
|
||||||
|
case JunctionDunGen junc:
|
||||||
|
await PostGen(junc, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case MiddleConnectionDunGen dordor:
|
||||||
|
await PostGen(dordor, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case DungeonEntranceDunGen entrance:
|
||||||
|
await PostGen(entrance, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case ExternalWindowDunGen externalWindow:
|
||||||
|
await PostGen(externalWindow, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case InternalWindowDunGen internalWindow:
|
||||||
|
await PostGen(internalWindow, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case MobsDunGen mob:
|
||||||
|
await PostGen(mob, dungeons[^1], random);
|
||||||
|
break;
|
||||||
|
case NoiseDistanceDunGen distance:
|
||||||
|
dungeons.Add(await GenerateNoiseDistanceDunGen(position, distance, reservedTiles, seed, random));
|
||||||
|
break;
|
||||||
|
case NoiseDunGen noise:
|
||||||
|
dungeons.Add(await GenerateNoiseDunGen(position, noise, reservedTiles, seed, random));
|
||||||
|
break;
|
||||||
|
case OreDunGen ore:
|
||||||
|
await PostGen(ore, dungeons[^1], random);
|
||||||
|
break;
|
||||||
|
case PrefabDunGen prefab:
|
||||||
|
dungeons.Add(await GeneratePrefabDunGen(position, data, prefab, reservedTiles, random));
|
||||||
|
break;
|
||||||
|
case PrototypeDunGen prototypo:
|
||||||
|
var groupConfig = _prototype.Index(prototypo.Proto);
|
||||||
|
position = (position + random.NextPolarVector2(groupConfig.MinOffset, groupConfig.MaxOffset)).Floored();
|
||||||
|
|
||||||
|
var dataCopy = groupConfig.Data.Clone();
|
||||||
|
dataCopy.Apply(data);
|
||||||
|
|
||||||
|
dungeons.AddRange(await GetDungeons(position, groupConfig, dataCopy, groupConfig.Layers, reservedTiles, seed, random));
|
||||||
|
break;
|
||||||
|
case ReplaceTileDunGen replace:
|
||||||
|
dungeons.Add(await GenerateTileReplacementDunGen(replace, data, reservedTiles, random));
|
||||||
|
break;
|
||||||
|
case RoomEntranceDunGen rEntrance:
|
||||||
|
await PostGen(rEntrance, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case SplineDungeonConnectorDunGen spline:
|
||||||
|
dungeons.Add(await PostGen(spline, data, dungeons, reservedTiles, random));
|
||||||
|
break;
|
||||||
|
case WallMountDunGen wall:
|
||||||
|
await PostGen(wall, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
case WormCorridorDunGen worm:
|
||||||
|
await PostGen(worm, data, dungeons[^1], reservedTiles, random);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogDataError(Type type)
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Unable to find dungeon data keys for {type}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
private bool ValidateResume()
|
||||||
|
{
|
||||||
|
if (_entManager.Deleted(_gridUid))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wrapper around <see cref="Job{T}.SuspendIfOutOfTime"/>
|
||||||
|
/// </summary>
|
||||||
|
private async Task SuspendDungeon()
|
||||||
|
{
|
||||||
|
if (!TimeSlice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await SuspendIfOutOfTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ public sealed partial class DungeonSystem
|
|||||||
dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
|
dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
|
||||||
dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid);
|
dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid);
|
||||||
EntityManager.InitializeAndStartEntity(dungeonUid, mapId);
|
EntityManager.InitializeAndStartEntity(dungeonUid, mapId);
|
||||||
|
// If we created a grid (e.g. space dungen) then offset it so we don't double-apply positions
|
||||||
|
position = Vector2i.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
int seed;
|
int seed;
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public sealed partial class DungeonSystem
|
|||||||
Vector2i origin,
|
Vector2i origin,
|
||||||
DungeonRoomPrototype room,
|
DungeonRoomPrototype room,
|
||||||
Random random,
|
Random random,
|
||||||
|
HashSet<Vector2i>? reservedTiles,
|
||||||
bool clearExisting = false,
|
bool clearExisting = false,
|
||||||
bool rotation = false)
|
bool rotation = false)
|
||||||
{
|
{
|
||||||
@@ -78,7 +79,7 @@ public sealed partial class DungeonSystem
|
|||||||
var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation);
|
var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation);
|
||||||
var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform);
|
var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform);
|
||||||
|
|
||||||
SpawnRoom(gridUid, grid, finalTransform, room, clearExisting);
|
SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
|
public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
|
||||||
@@ -103,6 +104,7 @@ public sealed partial class DungeonSystem
|
|||||||
MapGridComponent grid,
|
MapGridComponent grid,
|
||||||
Matrix3x2 roomTransform,
|
Matrix3x2 roomTransform,
|
||||||
DungeonRoomPrototype room,
|
DungeonRoomPrototype room,
|
||||||
|
HashSet<Vector2i>? reservedTiles = null,
|
||||||
bool clearExisting = false)
|
bool clearExisting = false)
|
||||||
{
|
{
|
||||||
// Ensure the underlying template exists.
|
// Ensure the underlying template exists.
|
||||||
@@ -150,6 +152,10 @@ public sealed partial class DungeonSystem
|
|||||||
|
|
||||||
var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
|
var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
|
||||||
var rounded = tilePos.Floored();
|
var rounded = tilePos.Floored();
|
||||||
|
|
||||||
|
if (!clearExisting && reservedTiles?.Contains(rounded) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
_tiles.Add((rounded, tileRef.Tile));
|
_tiles.Add((rounded, tileRef.Tile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +171,10 @@ public sealed partial class DungeonSystem
|
|||||||
{
|
{
|
||||||
var templateXform = _xformQuery.GetComponent(templateEnt);
|
var templateXform = _xformQuery.GetComponent(templateEnt);
|
||||||
var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
|
var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
|
||||||
|
|
||||||
|
if (!clearExisting && reservedTiles?.Contains(childPos.Floored()) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
var childRot = templateXform.LocalRotation + finalRoomRotation;
|
var childRot = templateXform.LocalRotation + finalRoomRotation;
|
||||||
var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
|
var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
|
||||||
|
|
||||||
@@ -192,8 +202,11 @@ public sealed partial class DungeonSystem
|
|||||||
// Offset by 0.5 because decals are offset from bot-left corner
|
// Offset by 0.5 because decals are offset from bot-left corner
|
||||||
// So we convert it to center of tile then convert it back again after transform.
|
// So we convert it to center of tile then convert it back again after transform.
|
||||||
// Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
|
// Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
|
||||||
var position = Vector2.Transform(decal.Coordinates + Vector2Helpers.Half - roomCenter, roomTransform);
|
var position = Vector2.Transform(decal.Coordinates + grid.TileSizeHalfVector - roomCenter, roomTransform);
|
||||||
position -= Vector2Helpers.Half;
|
position -= grid.TileSizeHalfVector;
|
||||||
|
|
||||||
|
if (!clearExisting && reservedTiles?.Contains(position.Floored()) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Umm uhh I love decals so uhhhh idk what to do about this
|
// Umm uhh I love decals so uhhhh idk what to do about this
|
||||||
var angle = (decal.Angle + finalRoomRotation).Reduced();
|
var angle = (decal.Angle + finalRoomRotation).Reduced();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Content.Shared.Physics;
|
|||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Tag;
|
using Content.Shared.Tag;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -49,7 +50,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
|
|||||||
public const int CollisionLayer = (int) CollisionGroup.Impassable;
|
public const int CollisionLayer = (int) CollisionGroup.Impassable;
|
||||||
|
|
||||||
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
|
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
|
||||||
private readonly Dictionary<DungeonJob, CancellationTokenSource> _dungeonJobs = new();
|
private readonly Dictionary<DungeonJob.DungeonJob, CancellationTokenSource> _dungeonJobs = new();
|
||||||
|
|
||||||
[ValidatePrototypeId<ContentTileDefinition>]
|
[ValidatePrototypeId<ContentTileDefinition>]
|
||||||
public const string FallbackTileId = "FloorSteel";
|
public const string FallbackTileId = "FloorSteel";
|
||||||
@@ -190,18 +191,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
|
|||||||
int seed)
|
int seed)
|
||||||
{
|
{
|
||||||
var cancelToken = new CancellationTokenSource();
|
var cancelToken = new CancellationTokenSource();
|
||||||
var job = new DungeonJob(
|
var job = new DungeonJob.DungeonJob(
|
||||||
Log,
|
Log,
|
||||||
DungeonJobTime,
|
DungeonJobTime,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
_mapManager,
|
|
||||||
_prototype,
|
_prototype,
|
||||||
_tileDefManager,
|
_tileDefManager,
|
||||||
_anchorable,
|
_anchorable,
|
||||||
_decals,
|
_decals,
|
||||||
this,
|
this,
|
||||||
_lookup,
|
_lookup,
|
||||||
_tag,
|
|
||||||
_tile,
|
_tile,
|
||||||
_transform,
|
_transform,
|
||||||
gen,
|
gen,
|
||||||
@@ -215,7 +214,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
|
|||||||
_dungeonJobQueue.EnqueueJob(job);
|
_dungeonJobQueue.EnqueueJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dungeon> GenerateDungeonAsync(
|
public async Task<List<Dungeon>> GenerateDungeonAsync(
|
||||||
DungeonConfigPrototype gen,
|
DungeonConfigPrototype gen,
|
||||||
EntityUid gridUid,
|
EntityUid gridUid,
|
||||||
MapGridComponent grid,
|
MapGridComponent grid,
|
||||||
@@ -223,18 +222,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
|
|||||||
int seed)
|
int seed)
|
||||||
{
|
{
|
||||||
var cancelToken = new CancellationTokenSource();
|
var cancelToken = new CancellationTokenSource();
|
||||||
var job = new DungeonJob(
|
var job = new DungeonJob.DungeonJob(
|
||||||
Log,
|
Log,
|
||||||
DungeonJobTime,
|
DungeonJobTime,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
_mapManager,
|
|
||||||
_prototype,
|
_prototype,
|
||||||
_tileDefManager,
|
_tileDefManager,
|
||||||
_anchorable,
|
_anchorable,
|
||||||
_decals,
|
_decals,
|
||||||
this,
|
this,
|
||||||
_lookup,
|
_lookup,
|
||||||
_tag,
|
|
||||||
_tile,
|
_tile,
|
||||||
_transform,
|
_transform,
|
||||||
gen,
|
gen,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public sealed class RoomFillSystem : EntitySystem
|
|||||||
_maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
|
_maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
|
||||||
room,
|
room,
|
||||||
random,
|
random,
|
||||||
|
null,
|
||||||
clearExisting: component.ClearExisting,
|
clearExisting: component.ClearExisting,
|
||||||
rotation: component.Rotation);
|
rotation: component.Rotation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,9 +176,11 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
|
|||||||
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
|
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
|
||||||
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
|
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
|
||||||
var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
|
var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
|
||||||
var dungeon = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
|
var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
|
||||||
_missionParams.Seed));
|
_missionParams.Seed));
|
||||||
|
|
||||||
|
var dungeon = dungeons.First();
|
||||||
|
|
||||||
// Aborty
|
// Aborty
|
||||||
if (dungeon.Rooms.Count == 0)
|
if (dungeon.Rooms.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Content.Server.Shuttles.Systems;
|
using Content.Server.Shuttles.Systems;
|
||||||
|
using Content.Shared.Dataset;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -14,39 +16,92 @@ public sealed partial class GridSpawnComponent : Component
|
|||||||
/// Dictionary of groups where each group will have entries selected.
|
/// Dictionary of groups where each group will have entries selected.
|
||||||
/// String is just an identifier to make yaml easier.
|
/// String is just an identifier to make yaml easier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField(required: true)] public Dictionary<string, GridSpawnGroup> Groups = new();
|
[DataField(required: true)] public Dictionary<string, IGridSpawnGroup> Groups = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataRecord]
|
public interface IGridSpawnGroup
|
||||||
public record struct GridSpawnGroup
|
|
||||||
{
|
{
|
||||||
public List<ResPath> Paths = new();
|
/// <summary>
|
||||||
public int MinCount = 1;
|
/// Minimum distance to spawn away from the station.
|
||||||
public int MaxCount = 1;
|
/// </summary>
|
||||||
|
public float MinimumDistance { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
int MinCount { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
int MaxCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Components to be added to any spawned grids.
|
/// Components to be added to any spawned grids.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ComponentRegistry AddComponents = new();
|
public ComponentRegistry AddComponents { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hide the IFF label of the grid.
|
/// Hide the IFF label of the grid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Hide = false;
|
public bool Hide { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should we set the metadata name of a grid. Useful for admin purposes.
|
/// Should we set the metadata name of a grid. Useful for admin purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool NameGrid = false;
|
public bool NameGrid { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should we add this to the station's grids (if possible / relevant).
|
/// Should we add this to the station's grids (if possible / relevant).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool StationGrid = true;
|
public bool StationGrid { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public GridSpawnGroup()
|
[DataRecord]
|
||||||
{
|
public sealed class DungeonSpawnGroup : IGridSpawnGroup
|
||||||
}
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prototypes we can choose from to spawn.
|
||||||
|
/// </summary>
|
||||||
|
public List<ProtoId<DungeonConfigPrototype>> Protos = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public float MinimumDistance { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int MinCount { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int MaxCount { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ComponentRegistry AddComponents { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Hide { get; set; } = false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool NameGrid { get; set; } = false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool StationGrid { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataRecord]
|
||||||
|
public sealed class GridSpawnGroup : IGridSpawnGroup
|
||||||
|
{
|
||||||
|
public List<ResPath> Paths = new();
|
||||||
|
|
||||||
|
public float MinimumDistance { get; }
|
||||||
|
public ProtoId<DatasetPrototype>? NameDataset { get; }
|
||||||
|
public int MinCount { get; set; } = 1;
|
||||||
|
public int MaxCount { get; set; } = 1;
|
||||||
|
public ComponentRegistry AddComponents { get; set; } = new();
|
||||||
|
public bool Hide { get; set; } = false;
|
||||||
|
public bool NameGrid { get; set; } = true;
|
||||||
|
public bool StationGrid { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
using System.Numerics;
|
||||||
using Content.Server.Shuttles.Components;
|
using Content.Server.Shuttles.Components;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
using Content.Server.Station.Events;
|
using Content.Server.Station.Events;
|
||||||
using Content.Shared.Cargo.Components;
|
using Content.Shared.Cargo.Components;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.Procedural;
|
||||||
|
using Content.Shared.Salvage;
|
||||||
using Content.Shared.Shuttles.Components;
|
using Content.Shared.Shuttles.Components;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -80,6 +85,76 @@ public sealed partial class ShuttleSystem
|
|||||||
_mapManager.DeleteMap(mapId);
|
_mapManager.DeleteMap(mapId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryDungeonSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, DungeonSpawnGroup group, out EntityUid spawned)
|
||||||
|
{
|
||||||
|
spawned = EntityUid.Invalid;
|
||||||
|
var dungeonProtoId = _random.Pick(group.Protos);
|
||||||
|
|
||||||
|
if (!_protoManager.TryIndex(dungeonProtoId, out var dungeonProto))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spawnCoords = new EntityCoordinates(targetGrid, Vector2.Zero);
|
||||||
|
|
||||||
|
if (group.MinimumDistance > 0f)
|
||||||
|
{
|
||||||
|
spawnCoords = spawnCoords.Offset(_random.NextVector2(group.MinimumDistance, group.MinimumDistance * 1.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
var spawnMapCoords = _transform.ToMapCoordinates(spawnCoords);
|
||||||
|
var spawnedGrid = _mapManager.CreateGridEntity(mapId);
|
||||||
|
|
||||||
|
_transform.SetMapCoordinates(spawnedGrid, spawnMapCoords);
|
||||||
|
_dungeon.GenerateDungeon(dungeonProto, spawnedGrid.Owner, spawnedGrid.Comp, Vector2i.Zero, _random.Next());
|
||||||
|
|
||||||
|
spawned = spawnedGrid.Owner;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGridSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, GridSpawnGroup group, out EntityUid spawned)
|
||||||
|
{
|
||||||
|
spawned = EntityUid.Invalid;
|
||||||
|
|
||||||
|
if (group.Paths.Count == 0)
|
||||||
|
{
|
||||||
|
Log.Error($"Found no paths for GridSpawn");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths = new ValueList<ResPath>();
|
||||||
|
|
||||||
|
// Round-robin so we try to avoid dupes where possible.
|
||||||
|
if (paths.Count == 0)
|
||||||
|
{
|
||||||
|
paths.AddRange(group.Paths);
|
||||||
|
_random.Shuffle(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = paths[^1];
|
||||||
|
paths.RemoveAt(paths.Count - 1);
|
||||||
|
|
||||||
|
if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
|
||||||
|
{
|
||||||
|
if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
|
||||||
|
{
|
||||||
|
TryFTLProximity(ent[0], targetGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.NameGrid)
|
||||||
|
{
|
||||||
|
var name = path.FilenameWithoutExtension;
|
||||||
|
_metadata.SetEntityName(ent[0], name);
|
||||||
|
}
|
||||||
|
|
||||||
|
spawned = ent[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error($"Error loading gridspawn for {ToPrettyString(stationUid)} / {path}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void GridSpawns(EntityUid uid, GridSpawnComponent component)
|
private void GridSpawns(EntityUid uid, GridSpawnComponent component)
|
||||||
{
|
{
|
||||||
if (!_cfg.GetCVar(CCVars.GridFill))
|
if (!_cfg.GetCVar(CCVars.GridFill))
|
||||||
@@ -97,81 +172,49 @@ public sealed partial class ShuttleSystem
|
|||||||
|
|
||||||
// Spawn on a dummy map and try to FTL if possible, otherwise dump it.
|
// Spawn on a dummy map and try to FTL if possible, otherwise dump it.
|
||||||
var mapId = _mapManager.CreateMap();
|
var mapId = _mapManager.CreateMap();
|
||||||
var valid = true;
|
|
||||||
var paths = new List<ResPath>();
|
|
||||||
|
|
||||||
foreach (var group in component.Groups.Values)
|
foreach (var group in component.Groups.Values)
|
||||||
{
|
{
|
||||||
if (group.Paths.Count == 0)
|
var count = _random.Next(group.MinCount, group.MaxCount + 1);
|
||||||
{
|
|
||||||
Log.Error($"Found no paths for GridSpawn");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = _random.Next(group.MinCount, group.MaxCount);
|
|
||||||
paths.Clear();
|
|
||||||
|
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
// Round-robin so we try to avoid dupes where possible.
|
EntityUid spawned;
|
||||||
if (paths.Count == 0)
|
|
||||||
|
switch (group)
|
||||||
{
|
{
|
||||||
paths.AddRange(group.Paths);
|
case DungeonSpawnGroup dungeon:
|
||||||
_random.Shuffle(paths);
|
if (!TryDungeonSpawn(targetGrid.Value, uid, mapId, dungeon, out spawned))
|
||||||
}
|
|
||||||
|
|
||||||
var path = paths[^1];
|
|
||||||
paths.RemoveAt(paths.Count - 1);
|
|
||||||
|
|
||||||
if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
|
|
||||||
{
|
|
||||||
if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
|
|
||||||
{
|
|
||||||
TryFTLProximity(ent[0], targetGrid.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.Hide)
|
|
||||||
{
|
|
||||||
var iffComp = EnsureComp<IFFComponent>(ent[0]);
|
|
||||||
iffComp.Flags |= IFFFlags.HideLabel;
|
|
||||||
Dirty(ent[0], iffComp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.StationGrid)
|
|
||||||
{
|
|
||||||
_station.AddGridToStation(uid, ent[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.NameGrid)
|
|
||||||
{
|
|
||||||
var name = path.FilenameWithoutExtension;
|
|
||||||
_metadata.SetEntityName(ent[0], name);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var compReg in group.AddComponents.Values)
|
|
||||||
{
|
|
||||||
var compType = compReg.Component.GetType();
|
|
||||||
|
|
||||||
if (HasComp(ent[0], compType))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var comp = _factory.GetComponent(compType);
|
break;
|
||||||
AddComp(ent[0], comp, true);
|
case GridSpawnGroup grid:
|
||||||
}
|
if (!TryGridSpawn(targetGrid.Value, uid, mapId, grid, out spawned))
|
||||||
}
|
continue;
|
||||||
else
|
|
||||||
{
|
break;
|
||||||
valid = false;
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!valid)
|
if (_protoManager.TryIndex(group.NameDataset, out var dataset))
|
||||||
{
|
{
|
||||||
Log.Error($"Error loading gridspawn for {ToPrettyString(uid)} / {path}");
|
_metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.Hide)
|
||||||
|
{
|
||||||
|
var iffComp = EnsureComp<IFFComponent>(spawned);
|
||||||
|
iffComp.Flags |= IFFFlags.HideLabel;
|
||||||
|
Dirty(spawned, iffComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.StationGrid)
|
||||||
|
{
|
||||||
|
_station.AddGridToStation(uid, spawned);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityManager.AddComponents(spawned, group.AddComponents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
|
|||||||
using Content.Server.Body.Systems;
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.Doors.Systems;
|
using Content.Server.Doors.Systems;
|
||||||
using Content.Server.Parallax;
|
using Content.Server.Parallax;
|
||||||
|
using Content.Server.Procedural;
|
||||||
using Content.Server.Shuttles.Components;
|
using Content.Server.Shuttles.Components;
|
||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
using Content.Server.Stunnable;
|
using Content.Server.Stunnable;
|
||||||
@@ -20,6 +21,7 @@ using Robust.Shared.Map.Components;
|
|||||||
using Robust.Shared.Physics;
|
using Robust.Shared.Physics;
|
||||||
using Robust.Shared.Physics.Components;
|
using Robust.Shared.Physics.Components;
|
||||||
using Robust.Shared.Physics.Systems;
|
using Robust.Shared.Physics.Systems;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
@@ -28,15 +30,18 @@ namespace Content.Server.Shuttles.Systems;
|
|||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed partial class ShuttleSystem : SharedShuttleSystem
|
public sealed partial class ShuttleSystem : SharedShuttleSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IAdminLogManager _logger = default!;
|
||||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
|
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly BiomeSystem _biomes = default!;
|
[Dependency] private readonly BiomeSystem _biomes = default!;
|
||||||
[Dependency] private readonly BodySystem _bobby = default!;
|
[Dependency] private readonly BodySystem _bobby = default!;
|
||||||
[Dependency] private readonly DockingSystem _dockSystem = default!;
|
[Dependency] private readonly DockingSystem _dockSystem = default!;
|
||||||
|
[Dependency] private readonly DungeonSystem _dungeon = default!;
|
||||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||||
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||||
[Dependency] private readonly MapLoaderSystem _loader = default!;
|
[Dependency] private readonly MapLoaderSystem _loader = default!;
|
||||||
@@ -52,7 +57,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
|
|||||||
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
||||||
[Dependency] private readonly ThrusterSystem _thruster = default!;
|
[Dependency] private readonly ThrusterSystem _thruster = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
[Dependency] private readonly IAdminLogManager _logger = default!;
|
|
||||||
|
|
||||||
public const float TileMassMultiplier = 0.5f;
|
public const float TileMassMultiplier = 0.5f;
|
||||||
|
|
||||||
|
|||||||
13
Content.Shared/Procedural/Components/EntityRemapComponent.cs
Normal file
13
Content.Shared/Procedural/Components/EntityRemapComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates this entity prototype should be re-mapped to another
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class EntityRemapComponent : Component
|
||||||
|
{
|
||||||
|
[DataField(required: true)]
|
||||||
|
public Dictionary<EntProtoId, EntProtoId> Mask = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Shared.Procedural.Distance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a rounder shape useful for more natural areas.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class DunGenEuclideanSquaredDistance : IDunGenDistance
|
||||||
|
{
|
||||||
|
[DataField]
|
||||||
|
public float BlendWeight { get; set; } = 0.50f;
|
||||||
|
}
|
||||||
10
Content.Shared/Procedural/Distance/DunGenSquareBump.cs
Normal file
10
Content.Shared/Procedural/Distance/DunGenSquareBump.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Shared.Procedural.Distance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a squarish-shape that's better for filling in most of the area.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class DunGenSquareBump : IDunGenDistance
|
||||||
|
{
|
||||||
|
[DataField]
|
||||||
|
public float BlendWeight { get; set; } = 0.50f;
|
||||||
|
}
|
||||||
14
Content.Shared/Procedural/Distance/IDunGenDistance.cs
Normal file
14
Content.Shared/Procedural/Distance/IDunGenDistance.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Content.Shared.Procedural.Distance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used if you want to limit the distance noise is generated by some arbitrary config
|
||||||
|
/// </summary>
|
||||||
|
[ImplicitDataDefinitionForInheritors]
|
||||||
|
public partial interface IDunGenDistance
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How much to blend between the original noise value and the adjusted one.
|
||||||
|
/// </summary>
|
||||||
|
float BlendWeight { get; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
namespace Content.Shared.Procedural;
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Procedurally generated dungeon data.
|
||||||
|
/// </summary>
|
||||||
public sealed class Dungeon
|
public sealed class Dungeon
|
||||||
{
|
{
|
||||||
public readonly List<DungeonRoom> Rooms;
|
public static Dungeon Empty = new Dungeon();
|
||||||
|
|
||||||
|
private List<DungeonRoom> _rooms;
|
||||||
|
private HashSet<Vector2i> _allTiles = new();
|
||||||
|
|
||||||
|
public IReadOnlyList<DungeonRoom> Rooms => _rooms;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hashset of the tiles across all rooms.
|
/// Hashset of the tiles across all rooms.
|
||||||
@@ -17,18 +25,64 @@ public sealed class Dungeon
|
|||||||
|
|
||||||
public readonly HashSet<Vector2i> Entrances = new();
|
public readonly HashSet<Vector2i> Entrances = new();
|
||||||
|
|
||||||
public Dungeon()
|
public IReadOnlySet<Vector2i> AllTiles => _allTiles;
|
||||||
|
|
||||||
|
public Dungeon() : this(new List<DungeonRoom>())
|
||||||
{
|
{
|
||||||
Rooms = new List<DungeonRoom>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dungeon(List<DungeonRoom> rooms)
|
public Dungeon(List<DungeonRoom> rooms)
|
||||||
{
|
{
|
||||||
Rooms = rooms;
|
// This reftype is mine now.
|
||||||
|
_rooms = rooms;
|
||||||
|
|
||||||
foreach (var room in Rooms)
|
foreach (var room in _rooms)
|
||||||
{
|
{
|
||||||
Entrances.UnionWith(room.Entrances);
|
InternalAddRoom(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshAllTiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshAllTiles()
|
||||||
|
{
|
||||||
|
_allTiles.Clear();
|
||||||
|
_allTiles.UnionWith(RoomTiles);
|
||||||
|
_allTiles.UnionWith(RoomExteriorTiles);
|
||||||
|
_allTiles.UnionWith(CorridorTiles);
|
||||||
|
_allTiles.UnionWith(CorridorExteriorTiles);
|
||||||
|
_allTiles.UnionWith(Entrances);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rebuild()
|
||||||
|
{
|
||||||
|
_allTiles.Clear();
|
||||||
|
|
||||||
|
RoomTiles.Clear();
|
||||||
|
RoomExteriorTiles.Clear();
|
||||||
|
Entrances.Clear();
|
||||||
|
|
||||||
|
foreach (var room in _rooms)
|
||||||
|
{
|
||||||
|
InternalAddRoom(room, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshAllTiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRoom(DungeonRoom room)
|
||||||
|
{
|
||||||
|
_rooms.Add(room);
|
||||||
|
InternalAddRoom(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InternalAddRoom(DungeonRoom room, bool refreshAll = true)
|
||||||
|
{
|
||||||
|
Entrances.UnionWith(room.Entrances);
|
||||||
|
RoomTiles.UnionWith(room.Tiles);
|
||||||
|
RoomExteriorTiles.UnionWith(room.Exterior);
|
||||||
|
|
||||||
|
if (refreshAll)
|
||||||
|
RefreshAllTiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
using Content.Shared.Procedural.DungeonGenerators;
|
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Shared.Procedural;
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
[Prototype("dungeonConfig")]
|
[Prototype]
|
||||||
public sealed partial class DungeonConfigPrototype : IPrototype
|
public sealed partial class DungeonConfigPrototype : IPrototype
|
||||||
{
|
{
|
||||||
[IdDataField]
|
[IdDataField]
|
||||||
public string ID { get; private set; } = default!;
|
public string ID { get; private set; } = default!;
|
||||||
|
|
||||||
[DataField("generator", required: true)]
|
/// <summary>
|
||||||
public IDunGen Generator = default!;
|
/// <see cref="Data"/>
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public DungeonData Data = DungeonData.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ran after the main dungeon is created.
|
/// The secret sauce, procedural generation layers that get run.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("postGeneration")]
|
[DataField(required: true)]
|
||||||
public List<IPostDunGen> PostGeneration = new();
|
public List<IDunGenLayer> Layers = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should we reserve the tiles generated by this config so no other dungeons can spawn on it within the same job?
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool ReserveTiles;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum times to run the config.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MinCount = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum times to run the config.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MaxCount = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum amount we can offset the dungeon by.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MinOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum amount we can offset the dungeon by.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MaxOffset;
|
||||||
}
|
}
|
||||||
|
|||||||
105
Content.Shared/Procedural/DungeonData.cs
Normal file
105
Content.Shared/Procedural/DungeonData.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Storage;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to set dungeon values for all layers.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This lets us share data between different dungeon configs without having to repeat entire configs.
|
||||||
|
/// </remarks>
|
||||||
|
[DataRecord]
|
||||||
|
public sealed class DungeonData
|
||||||
|
{
|
||||||
|
// I hate this but it also significantly reduces yaml bloat if we add like 10 variations on the same set of layers
|
||||||
|
// e.g. science rooms, engi rooms, cargo rooms all under PlanetBase for example.
|
||||||
|
// without having to do weird nesting. It also means we don't need to copy-paste the same prototype across several layers
|
||||||
|
// The alternative is doing like,
|
||||||
|
// 2 layer prototype, 1 layer with the specified data, 3 layer prototype, 2 layers with specified data, etc.
|
||||||
|
// As long as we just keep the code clean over time it won't be bad to maintain.
|
||||||
|
|
||||||
|
public static DungeonData Empty = new();
|
||||||
|
|
||||||
|
public Dictionary<DungeonDataKey, Color> Colors = new();
|
||||||
|
public Dictionary<DungeonDataKey, EntProtoId> Entities = new();
|
||||||
|
public Dictionary<DungeonDataKey, ProtoId<EntitySpawnEntryPrototype>> SpawnGroups = new();
|
||||||
|
public Dictionary<DungeonDataKey, ProtoId<ContentTileDefinition>> Tiles = new();
|
||||||
|
public Dictionary<DungeonDataKey, EntityWhitelist> Whitelists = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the specified data to this data.
|
||||||
|
/// </summary>
|
||||||
|
public void Apply(DungeonData data)
|
||||||
|
{
|
||||||
|
// Copy-paste moment.
|
||||||
|
foreach (var color in data.Colors)
|
||||||
|
{
|
||||||
|
Colors[color.Key] = color.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var color in data.Entities)
|
||||||
|
{
|
||||||
|
Entities[color.Key] = color.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var color in data.SpawnGroups)
|
||||||
|
{
|
||||||
|
SpawnGroups[color.Key] = color.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var color in data.Tiles)
|
||||||
|
{
|
||||||
|
Tiles[color.Key] = color.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var color in data.Whitelists)
|
||||||
|
{
|
||||||
|
Whitelists[color.Key] = color.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DungeonData Clone()
|
||||||
|
{
|
||||||
|
return new DungeonData
|
||||||
|
{
|
||||||
|
// Only shallow clones but won't matter for DungeonJob purposes.
|
||||||
|
Colors = Colors.ShallowClone(),
|
||||||
|
Entities = Entities.ShallowClone(),
|
||||||
|
SpawnGroups = SpawnGroups.ShallowClone(),
|
||||||
|
Tiles = Tiles.ShallowClone(),
|
||||||
|
Whitelists = Whitelists.ShallowClone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DungeonDataKey : byte
|
||||||
|
{
|
||||||
|
// Colors
|
||||||
|
Decals,
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
Cabling,
|
||||||
|
CornerWalls,
|
||||||
|
Fill,
|
||||||
|
Junction,
|
||||||
|
Walls,
|
||||||
|
|
||||||
|
// SpawnGroups
|
||||||
|
CornerClutter,
|
||||||
|
Entrance,
|
||||||
|
EntranceFlank,
|
||||||
|
WallMounts,
|
||||||
|
Window,
|
||||||
|
|
||||||
|
// Tiles
|
||||||
|
FallbackTile,
|
||||||
|
WidenTile,
|
||||||
|
|
||||||
|
// Whitelists
|
||||||
|
Rooms,
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the specified config on an exterior tile of the attached dungeon.
|
||||||
|
/// Useful if you're using <see cref="GroupDunGen"/> or otherwise want a dungeon on the outside of a grid.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ExteriorDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
[DataField(required: true)]
|
||||||
|
public ProtoId<DungeonConfigPrototype> Proto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills unreserved tiles with the specified entity prototype.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// DungeonData keys are:
|
||||||
|
/// - Fill
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class FillGridDunGen : IDunGenLayer;
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Content.Shared.Procedural.DungeonGenerators;
|
|
||||||
|
|
||||||
[ImplicitDataDefinitionForInheritors]
|
|
||||||
public partial interface IDunGen
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Content.Shared.Procedural.Distance;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Like <see cref="Content.Shared.Procedural.DungeonGenerators.NoiseDunGenLayer"/> except with maximum dimensions
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class NoiseDistanceDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
[DataField]
|
||||||
|
public IDunGenDistance? DistanceConfig;
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public Vector2i Size;
|
||||||
|
|
||||||
|
[DataField(required: true)]
|
||||||
|
public List<NoiseDunGenLayer> Layers = new();
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
using Content.Shared.Maps;
|
using Content.Shared.Procedural.Distance;
|
||||||
using Robust.Shared.Noise;
|
using Robust.Shared.Noise;
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.DungeonGenerators;
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates dungeon flooring based on the specified noise.
|
/// Generates dungeon flooring based on the specified noise.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class NoiseDunGen : IDunGen
|
public sealed partial class NoiseDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Floodfills out from 0 until it finds a valid tile.
|
* Floodfills out from 0 until it finds a valid tile.
|
||||||
|
|||||||
@@ -1,30 +1,20 @@
|
|||||||
using Content.Shared.Maps;
|
using Robust.Shared.Prototypes;
|
||||||
using Content.Shared.Tag;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.DungeonGenerators;
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist.
|
/// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class PrefabDunGen : IDunGen
|
/// <remarks>
|
||||||
|
/// DungeonData keys are:
|
||||||
|
/// - FallbackTile
|
||||||
|
/// - Rooms
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class PrefabDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Rooms need to match any of these tags
|
|
||||||
/// </summary>
|
|
||||||
[DataField("roomWhitelist", customTypeSerializer:typeof(PrototypeIdListSerializer<TagPrototype>))]
|
|
||||||
public List<string> RoomWhitelist = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Room pack presets we can use for this prefab.
|
/// Room pack presets we can use for this prefab.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("presets", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DungeonPresetPrototype>))]
|
[DataField(required: true)]
|
||||||
public List<string> Presets = new();
|
public List<ProtoId<DungeonPresetPrototype>> Presets = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fallback tile.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs another <see cref="DungeonConfigPrototype"/>.
|
||||||
|
/// Used for storing data on 1 system.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PrototypeDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
[DataField(required: true)]
|
||||||
|
public ProtoId<DungeonConfigPrototype> Proto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Content.Shared.Maps;
|
||||||
|
using Robust.Shared.Noise;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonGenerators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces existing tiles if they're not empty.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ReplaceTileDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Chance for a non-variant tile to be used, in case they're too noisy.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float VariantWeight = 0.1f;
|
||||||
|
|
||||||
|
[DataField(required: true)]
|
||||||
|
public List<ReplaceTileLayer> Layers = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataRecord]
|
||||||
|
public record struct ReplaceTileLayer
|
||||||
|
{
|
||||||
|
public ProtoId<ContentTileDefinition> Tile;
|
||||||
|
|
||||||
|
public float Threshold;
|
||||||
|
|
||||||
|
public FastNoiseLite Noise;
|
||||||
|
}
|
||||||
21
Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs
Normal file
21
Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Content.Shared.Storage;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonLayers;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns mobs inside of the dungeon randomly.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MobsDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
// Counts separate to config to avoid some duplication.
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public int MinCount = 1;
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public int MaxCount = 1;
|
||||||
|
|
||||||
|
[DataField(required: true)]
|
||||||
|
public List<EntitySpawnEntry> Groups = new();
|
||||||
|
}
|
||||||
42
Content.Shared/Procedural/DungeonLayers/OreDunGen.cs
Normal file
42
Content.Shared/Procedural/DungeonLayers/OreDunGen.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.DungeonLayers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates veins inside of the specified dungeon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Generates on top of existing entities for sanity reasons moreso than performance.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class OreDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// If the vein generation should occur on top of existing entities what are we replacing.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntProtoId? Replacement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity to spawn.
|
||||||
|
/// </summary>
|
||||||
|
[DataField(required: true)]
|
||||||
|
public EntProtoId Entity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum amount of group spawns
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int Count = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum entities to spawn in one group.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MinGroupSize = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum entities to spawn in one group.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MaxGroupSize = 1;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace Content.Shared.Procedural;
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
|
// TODO: Cache center and bounds and shit and don't make the caller deal with it.
|
||||||
public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior)
|
public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior)
|
||||||
{
|
{
|
||||||
public readonly List<Vector2i> Entrances = new();
|
public readonly List<Vector2i> Entrances = new();
|
||||||
|
|||||||
7
Content.Shared/Procedural/IDunGenLayer.cs
Normal file
7
Content.Shared/Procedural/IDunGenLayer.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
|
[ImplicitDataDefinitionForInheritors]
|
||||||
|
public partial interface IDunGenLayer
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs cables throughout the dungeon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// DungeonData keys are:
|
||||||
|
/// - Cabling
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class AutoCablingDunGen : IDunGenLayer;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Robust.Shared.Prototypes;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs cables throughout the dungeon.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class AutoCablingPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField]
|
|
||||||
public EntProtoId Entity = "CableApcExtension";
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Content.Shared.Parallax.Biomes;
|
using Content.Shared.Parallax.Biomes;
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
@@ -8,7 +7,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
/// Generates a biome on top of valid tiles, then removes the biome when done.
|
/// Generates a biome on top of valid tiles, then removes the biome when done.
|
||||||
/// Only works if no existing biome is present.
|
/// Only works if no existing biome is present.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class BiomePostGen : IPostDunGen
|
public sealed partial class BiomeDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
[DataField(required: true)]
|
[DataField(required: true)]
|
||||||
public ProtoId<BiomeTemplatePrototype> BiomeTemplate;
|
public ProtoId<BiomeTemplatePrototype> BiomeTemplate;
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using Content.Shared.Parallax.Biomes.Markers;
|
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
|
||||||
using Content.Shared.Random;
|
using Content.Shared.Random;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
@@ -8,7 +6,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawns the specified marker layer on top of the dungeon rooms.
|
/// Spawns the specified marker layer on top of the dungeon rooms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class BiomeMarkerLayerPostGen : IPostDunGen
|
public sealed partial class BiomeMarkerLayerDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many times to spawn marker layers; can duplicate.
|
/// How many times to spawn marker layers; can duplicate.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Iterates room edges and places the relevant tiles and walls on any free indices.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - CornerWalls (Optional)
|
||||||
|
/// - FallbackTile
|
||||||
|
/// - Walls
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class BoundaryWallDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
[DataField]
|
||||||
|
public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum BoundaryWallFlags : byte
|
||||||
|
{
|
||||||
|
Rooms = 1 << 0,
|
||||||
|
Corridors = 1 << 1,
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Iterates room edges and places the relevant tiles and walls on any free indices.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class BoundaryWallPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField]
|
|
||||||
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
|
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public EntProtoId Wall = "WallSolid";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Walls to use in corners if applicable.
|
|
||||||
/// </summary>
|
|
||||||
[DataField]
|
|
||||||
public string? CornerWall;
|
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum BoundaryWallFlags : byte
|
|
||||||
{
|
|
||||||
Rooms = 1 << 0,
|
|
||||||
Corridors = 1 << 1,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns entities inside corners.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - CornerClutter
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class CornerClutterDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
[DataField]
|
||||||
|
public float Chance = 0.50f;
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using Content.Shared.Storage;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Spawns entities inside corners.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class CornerClutterPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField]
|
|
||||||
public float Chance = 0.50f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The default starting bulbs
|
|
||||||
/// </summary>
|
|
||||||
[DataField(required: true)]
|
|
||||||
public List<EntitySpawnEntry> Contents = new();
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds entities randomly to the corridors.
|
/// Adds entities randomly to the corridors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class CorridorClutterPostGen : IPostDunGen
|
public sealed partial class CorridorClutterDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
[DataField]
|
[DataField]
|
||||||
public float Chance = 0.05f;
|
public float Chance = 0.05f;
|
||||||
@@ -7,29 +7,23 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies decal skirting to corridors.
|
/// Applies decal skirting to corridors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class CorridorDecalSkirtingPostGen : IPostDunGen
|
public sealed partial class CorridorDecalSkirtingDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Color to apply to decals.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("color")]
|
|
||||||
public Color? Color;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decal where 1 edge is found.
|
/// Decal where 1 edge is found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("cardinalDecals")]
|
[DataField]
|
||||||
public Dictionary<DirectionFlag, string> CardinalDecals = new();
|
public Dictionary<DirectionFlag, string> CardinalDecals = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decal where 1 corner edge is found.
|
/// Decal where 1 corner edge is found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("pocketDecals")]
|
[DataField]
|
||||||
public Dictionary<Direction, string> PocketDecals = new();
|
public Dictionary<Direction, string> PocketDecals = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decal where 2 or 3 edges are found.
|
/// Decal where 2 or 3 edges are found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("cornerDecals")]
|
[DataField]
|
||||||
public Dictionary<DirectionFlag, string> CornerDecals = new();
|
public Dictionary<DirectionFlag, string> CornerDecals = new();
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connects room entrances via corridor segments.
|
/// Connects room entrances via corridor segments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class CorridorPostGen : IPostDunGen
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - FallbackTile
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class CorridorDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How far we're allowed to generate a corridor before calling it.
|
/// How far we're allowed to generate a corridor before calling it.
|
||||||
@@ -17,9 +18,6 @@ public sealed partial class CorridorPostGen : IPostDunGen
|
|||||||
[DataField]
|
[DataField]
|
||||||
public int PathLimit = 2048;
|
public int PathLimit = 2048;
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How wide to make the corridor.
|
/// How wide to make the corridor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects [count] rooms and places external doors to them.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - Entrance
|
||||||
|
/// - FallbackTile
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class DungeonEntranceDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How many rooms we place doors on.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int Count = 1;
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects [count] rooms and places external doors to them.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class DungeonEntrancePostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// How many rooms we place doors on.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("count")]
|
|
||||||
public int Count = 1;
|
|
||||||
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"CableApcExtension",
|
|
||||||
"AirlockGlass",
|
|
||||||
};
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns entities on either side of an entrance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - FallbackTile
|
||||||
|
/// -
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class EntranceFlankDunGen : IDunGenLayer;
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Spawns entities on either side of an entrance.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class EntranceFlankPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
|
|
||||||
[DataField("entities")]
|
|
||||||
public List<string?> Entities = new();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If external areas are found will try to generate windows.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - EntranceFlank
|
||||||
|
/// - FallbackTile
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class ExternalWindowDunGen : IDunGenLayer;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If external areas are found will try to generate windows.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class ExternalWindowPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"Grille",
|
|
||||||
"Window",
|
|
||||||
};
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ran after generating dungeon rooms. Can be used for additional loot, contents, etc.
|
|
||||||
/// </summary>
|
|
||||||
[ImplicitDataDefinitionForInheritors]
|
|
||||||
public partial interface IPostDunGen
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If internal areas are found will try to generate windows.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - FallbackTile
|
||||||
|
/// - Window
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class InternalWindowDunGen : IDunGenLayer;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If internal areas are found will try to generate windows.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class InternalWindowPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"Grille",
|
|
||||||
"Window",
|
|
||||||
};
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
}
|
|
||||||
18
Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs
Normal file
18
Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places the specified entities at junction areas.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dungeon data keys are:
|
||||||
|
/// - Entrance
|
||||||
|
/// - FallbackTile
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class JunctionDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width to check for junctions.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int Width = 3;
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Places the specified entities at junction areas.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class JunctionPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Width to check for junctions.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("width")]
|
|
||||||
public int Width = 3;
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"CableApcExtension",
|
|
||||||
"AirlockGlass"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places the specified entities on the middle connections between rooms
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MiddleConnectionDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How much overlap there needs to be between 2 rooms exactly.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int OverlapCount = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many connections to spawn between rooms.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int Count = 1;
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Places the specified entities on the middle connections between rooms
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class MiddleConnectionPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// How much overlap there needs to be between 2 rooms exactly.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("overlapCount")]
|
|
||||||
public int OverlapCount = -1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many connections to spawn between rooms.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("count")]
|
|
||||||
public int Count = 1;
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"CableApcExtension",
|
|
||||||
"AirlockGlass"
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If overlap > 1 then what should spawn on the edges.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("edgeEntities")] public List<string?> EdgeEntities = new();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places tiles / entities onto room entrances.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// DungeonData keys are:
|
||||||
|
/// - Entrance
|
||||||
|
/// - FallbackTile
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class RoomEntranceDunGen : IDunGenLayer;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Places tiles / entities onto room entrances.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class RoomEntrancePostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
|
||||||
public List<string?> Entities = new()
|
|
||||||
{
|
|
||||||
"CableApcExtension",
|
|
||||||
"AirlockGlass",
|
|
||||||
};
|
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects dungeons via points that get subdivided.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int DivisionDistance = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much each subdivision can vary from the middle.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float VarianceMax = 0.35f;
|
||||||
|
}
|
||||||
13
Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs
Normal file
13
Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns on the boundary tiles of rooms.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class WallMountDunGen : IDunGenLayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Chance per free tile to spawn a wallmount.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public double Prob = 0.1;
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Content.Shared.Storage;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Spawns on the boundary tiles of rooms.
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class WallMountPostGen : IPostDunGen
|
|
||||||
{
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
|
||||||
public string Tile = "FloorSteel";
|
|
||||||
|
|
||||||
[DataField("spawns")]
|
|
||||||
public List<EntitySpawnEntry> Spawns = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Chance per free tile to spawn a wallmount.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("prob")]
|
|
||||||
public double Prob = 0.1;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
using Content.Shared.Maps;
|
|
||||||
using Content.Shared.Procedural.DungeonGenerators;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
|
|
||||||
namespace Content.Shared.Procedural.PostGeneration;
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
// Ime a worm
|
// Ime a worm
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates worm corridors.
|
/// Generates worm corridors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class WormCorridorPostGen : IPostDunGen
|
public sealed partial class WormCorridorDunGen : IDunGenLayer
|
||||||
{
|
{
|
||||||
[DataField]
|
[DataField]
|
||||||
public int PathLimit = 2048;
|
public int PathLimit = 2048;
|
||||||
@@ -31,9 +27,6 @@ public sealed partial class WormCorridorPostGen : IPostDunGen
|
|||||||
[DataField]
|
[DataField]
|
||||||
public Angle MaxAngleChange = Angle.FromDegrees(45);
|
public Angle MaxAngleChange = Angle.FromDegrees(45);
|
||||||
|
|
||||||
[DataField]
|
|
||||||
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How wide to make the corridor.
|
/// How wide to make the corridor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -32,14 +32,14 @@ public abstract partial class SharedSalvageSystem
|
|||||||
var layers = new Dictionary<string, int>();
|
var layers = new Dictionary<string, int>();
|
||||||
|
|
||||||
// If we ever add more random layers will need to Next on these.
|
// If we ever add more random layers will need to Next on these.
|
||||||
foreach (var layer in configProto.PostGeneration)
|
foreach (var layer in configProto.Layers)
|
||||||
{
|
{
|
||||||
switch (layer)
|
switch (layer)
|
||||||
{
|
{
|
||||||
case BiomePostGen:
|
case BiomeDunGen:
|
||||||
rand.Next();
|
rand.Next();
|
||||||
break;
|
break;
|
||||||
case BiomeMarkerLayerPostGen marker:
|
case BiomeMarkerLayerDunGen marker:
|
||||||
for (var i = 0; i < marker.Count; i++)
|
for (var i = 0; i < marker.Count; i++)
|
||||||
{
|
{
|
||||||
var proto = _proto.Index(marker.MarkerTemplate).Pick(rand);
|
var proto = _proto.Index(marker.MarkerTemplate).Pick(rand);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public abstract partial class SharedShuttleSystem : EntitySystem
|
|||||||
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
||||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||||
|
|
||||||
public const float FTLRange = 512f;
|
public const float FTLRange = 256f;
|
||||||
public const float FTLBufferRange = 8f;
|
public const float FTLBufferRange = 8f;
|
||||||
|
|
||||||
private EntityQuery<MapGridComponent> _gridQuery;
|
private EntityQuery<MapGridComponent> _gridQuery;
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
|
|||||||
|
|
||||||
namespace Content.Shared.Storage;
|
namespace Content.Shared.Storage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype wrapper around <see cref="EntitySpawnEntry"/>
|
||||||
|
/// </summary>
|
||||||
|
[Prototype]
|
||||||
|
public sealed class EntitySpawnEntryPrototype : IPrototype
|
||||||
|
{
|
||||||
|
[IdDataField]
|
||||||
|
public string ID { get; } = string.Empty;
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public List<EntitySpawnEntry> Entries = new();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dictates a list of items that can be spawned.
|
/// Dictates a list of items that can be spawned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -46,17 +46,26 @@
|
|||||||
path: /Maps/Shuttles/cargo.yml
|
path: /Maps/Shuttles/cargo.yml
|
||||||
- type: GridSpawn
|
- type: GridSpawn
|
||||||
groups:
|
groups:
|
||||||
trade:
|
vgroid: !type:DungeonSpawnGroup
|
||||||
|
minimumDistance: 1000
|
||||||
|
nameDataset: names_borer
|
||||||
|
addComponents:
|
||||||
|
- type: Gravity
|
||||||
|
enabled: true
|
||||||
|
inherent: true
|
||||||
|
protos:
|
||||||
|
- VGRoid
|
||||||
|
trade: !type:GridSpawnGroup
|
||||||
addComponents:
|
addComponents:
|
||||||
- type: ProtectedGrid
|
- type: ProtectedGrid
|
||||||
- type: TradeStation
|
- type: TradeStation
|
||||||
paths:
|
paths:
|
||||||
- /Maps/Shuttles/trading_outpost.yml
|
- /Maps/Shuttles/trading_outpost.yml
|
||||||
mining:
|
mining: !type:GridSpawnGroup
|
||||||
paths:
|
paths:
|
||||||
- /Maps/Shuttles/mining.yml
|
- /Maps/Shuttles/mining.yml
|
||||||
# Spawn last
|
# Spawn last
|
||||||
ruins:
|
ruins: !type:GridSpawnGroup
|
||||||
hide: true
|
hide: true
|
||||||
nameGrid: true
|
nameGrid: true
|
||||||
minCount: 2
|
minCount: 2
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long.
|
#TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long.
|
||||||
|
|
||||||
|
# Anyway
|
||||||
|
# See WallRock variants for the remappings.
|
||||||
|
|
||||||
#Asteroid rock
|
#Asteroid rock
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -639,21 +641,28 @@
|
|||||||
description: An ore vein rich with coal.
|
description: An ore vein rich with coal.
|
||||||
suffix: Coal
|
suffix: Coal
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreCoal
|
AsteroidRock: AsteroidRockCoal
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltCoal
|
||||||
layers:
|
WallRockChromite: WallRockChromiteCoal
|
||||||
- state: rock
|
WallRockSand: WallRockSandCoal
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowCoal
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreCoal
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_coal
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_coal
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockGold
|
id: WallRockGold
|
||||||
@@ -661,21 +670,28 @@
|
|||||||
description: An ore vein rich with gold.
|
description: An ore vein rich with gold.
|
||||||
suffix: Gold
|
suffix: Gold
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreGold
|
AsteroidRock: AsteroidRockGold
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltGold
|
||||||
layers:
|
WallRockChromite: WallRockChromiteGold
|
||||||
- state: rock
|
WallRockSand: WallRockSandGold
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowGold
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreGold
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_gold
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_gold
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockPlasma
|
id: WallRockPlasma
|
||||||
@@ -683,21 +699,28 @@
|
|||||||
description: An ore vein rich with plasma.
|
description: An ore vein rich with plasma.
|
||||||
suffix: Plasma
|
suffix: Plasma
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OrePlasma
|
AsteroidRock: AsteroidRockPlasma
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltPlasma
|
||||||
layers:
|
WallRockChromite: WallRockChromitePlasma
|
||||||
- state: rock
|
WallRockSand: WallRockSandPlasma
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowPlasma
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OrePlasma
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_phoron
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_phoron
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockQuartz
|
id: WallRockQuartz
|
||||||
@@ -705,21 +728,28 @@
|
|||||||
description: An ore vein rich with quartz.
|
description: An ore vein rich with quartz.
|
||||||
suffix: Quartz
|
suffix: Quartz
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreSpaceQuartz
|
AsteroidRock: AsteroidRockQuartz
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltQuartz
|
||||||
layers:
|
WallRockChromite: WallRockChromiteQuartz
|
||||||
- state: rock
|
WallRockSand: WallRockSandQuartz
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowQuartz
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreSpaceQuartz
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_quartz
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_quartz
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockSilver
|
id: WallRockSilver
|
||||||
@@ -727,21 +757,28 @@
|
|||||||
description: An ore vein rich with silver.
|
description: An ore vein rich with silver.
|
||||||
suffix: Silver
|
suffix: Silver
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreSilver
|
AsteroidRock: AsteroidRockSilver
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltSilver
|
||||||
layers:
|
WallRockChromite: WallRockChromiteSilver
|
||||||
- state: rock
|
WallRockSand: WallRockSandSilver
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowSilver
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreSilver
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_silver
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_silver
|
||||||
|
|
||||||
# Yes I know it drops steel but we may get smelting at some point
|
# Yes I know it drops steel but we may get smelting at some point
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -750,6 +787,13 @@
|
|||||||
description: An ore vein rich with iron.
|
description: An ore vein rich with iron.
|
||||||
suffix: Iron
|
suffix: Iron
|
||||||
components:
|
components:
|
||||||
|
- type: EntityRemap
|
||||||
|
mask:
|
||||||
|
AsteroidRock: AsteroidRockTin
|
||||||
|
WallRockBasalt: WallRockBasaltTin
|
||||||
|
WallRockChromite: WallRockChromiteTin
|
||||||
|
WallRockSand: WallRockSandTin
|
||||||
|
WallRockSnow: WallRockSnowTin
|
||||||
- type: OreVein
|
- type: OreVein
|
||||||
oreChance: 1.0
|
oreChance: 1.0
|
||||||
currentOre: OreSteel
|
currentOre: OreSteel
|
||||||
@@ -772,21 +816,28 @@
|
|||||||
description: An ore vein rich with uranium.
|
description: An ore vein rich with uranium.
|
||||||
suffix: Uranium
|
suffix: Uranium
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreUranium
|
AsteroidRock: AsteroidRockUranium
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltUranium
|
||||||
layers:
|
WallRockChromite: WallRockChromiteUranium
|
||||||
- state: rock
|
WallRockSand: WallRockSandUranium
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowUranium
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreUranium
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_uranium
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_uranium
|
||||||
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -795,21 +846,28 @@
|
|||||||
description: An ore vein rich with bananium.
|
description: An ore vein rich with bananium.
|
||||||
suffix: Bananium
|
suffix: Bananium
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreBananium
|
AsteroidRock: AsteroidRockBananium
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltBananium
|
||||||
layers:
|
WallRockChromite: WallRockChromiteBananium
|
||||||
- state: rock
|
WallRockSand: WallRockSandBananium
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowBananium
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreBananium
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_bananium
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_bananium
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockArtifactFragment
|
id: WallRockArtifactFragment
|
||||||
@@ -817,21 +875,28 @@
|
|||||||
description: A rock wall. What's that sticking out of it?
|
description: A rock wall. What's that sticking out of it?
|
||||||
suffix: Artifact Fragment
|
suffix: Artifact Fragment
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreArtifactFragment
|
AsteroidRock: AsteroidRockArtifactFragment
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltArtifactFragment
|
||||||
layers:
|
WallRockChromite: WallRockChromiteArtifactFragment
|
||||||
- state: rock
|
WallRockSand: WallRockSandArtifactFragment
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowArtifactFragment
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreArtifactFragment
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_artifact_fragment
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_artifact_fragment
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: WallRockSalt
|
id: WallRockSalt
|
||||||
@@ -839,21 +904,28 @@
|
|||||||
description: An ore vein rich with salt.
|
description: An ore vein rich with salt.
|
||||||
suffix: Salt
|
suffix: Salt
|
||||||
components:
|
components:
|
||||||
- type: OreVein
|
- type: EntityRemap
|
||||||
oreChance: 1.0
|
mask:
|
||||||
currentOre: OreSalt
|
AsteroidRock: AsteroidRockSalt
|
||||||
- type: Sprite
|
WallRockBasalt: WallRockBasaltSalt
|
||||||
layers:
|
WallRockChromite: WallRockChromiteSalt
|
||||||
- state: rock
|
WallRockSand: WallRockSandSalt
|
||||||
- map: [ "enum.EdgeLayer.South" ]
|
WallRockSnow: WallRockSnowSalt
|
||||||
state: rock_south
|
- type: OreVein
|
||||||
- map: [ "enum.EdgeLayer.East" ]
|
oreChance: 1.0
|
||||||
state: rock_east
|
currentOre: OreSalt
|
||||||
- map: [ "enum.EdgeLayer.North" ]
|
- type: Sprite
|
||||||
state: rock_north
|
layers:
|
||||||
- map: [ "enum.EdgeLayer.West" ]
|
- state: rock
|
||||||
state: rock_west
|
- map: [ "enum.EdgeLayer.South" ]
|
||||||
- state: rock_salt
|
state: rock_south
|
||||||
|
- map: [ "enum.EdgeLayer.East" ]
|
||||||
|
state: rock_east
|
||||||
|
- map: [ "enum.EdgeLayer.North" ]
|
||||||
|
state: rock_north
|
||||||
|
- map: [ "enum.EdgeLayer.West" ]
|
||||||
|
state: rock_west
|
||||||
|
- state: rock_salt
|
||||||
|
|
||||||
# Basalt variants
|
# Basalt variants
|
||||||
- type: entity
|
- type: entity
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user