Dungeon generation refactor (#17121)
This commit is contained in:
@@ -16,25 +16,12 @@ public sealed partial class PathfindingSystem
|
|||||||
return dx + dy;
|
return dx + dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float ManhattanDistance(Vector2i start, Vector2i end)
|
|
||||||
{
|
|
||||||
var distance = end - start;
|
|
||||||
return Math.Abs(distance.X) + Math.Abs(distance.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float OctileDistance(PathPoly start, PathPoly end)
|
public float OctileDistance(PathPoly start, PathPoly end)
|
||||||
{
|
{
|
||||||
var (dx, dy) = GetDiff(start, end);
|
var (dx, dy) = GetDiff(start, end);
|
||||||
return dx + dy + (1.41f - 2) * Math.Min(dx, dy);
|
return dx + dy + (1.41f - 2) * Math.Min(dx, dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float OctileDistance(Vector2i start, Vector2i end)
|
|
||||||
{
|
|
||||||
var diff = start - end;
|
|
||||||
var ab = Vector2.Abs(diff);
|
|
||||||
return ab.X + ab.Y + (1.41f - 2) * Math.Min(ab.X, ab.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 GetDiff(PathPoly start, PathPoly end)
|
private Vector2 GetDiff(PathPoly start, PathPoly end)
|
||||||
{
|
{
|
||||||
var startPos = start.Box.Center;
|
var startPos = start.Box.Center;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public sealed partial class DungeonJob
|
|||||||
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
|
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
|
||||||
var dungeonTransform = Matrix3.CreateTransform(_position, dungeonRotation);
|
var dungeonTransform = Matrix3.CreateTransform(_position, dungeonRotation);
|
||||||
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
|
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
|
||||||
var externalNodes = new Dictionary<DungeonRoomPackPrototype, HashSet<Vector2i>>();
|
|
||||||
var fallbackTile = new Tile(_tileDefManager[prefab.Tile].TileId);
|
var fallbackTile = new Tile(_tileDefManager[prefab.Tile].TileId);
|
||||||
|
|
||||||
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
|
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
|
||||||
@@ -28,21 +27,6 @@ public sealed partial class DungeonJob
|
|||||||
var size = pack.Size;
|
var size = pack.Size;
|
||||||
var sizePacks = roomPackProtos.GetOrNew(size);
|
var sizePacks = roomPackProtos.GetOrNew(size);
|
||||||
sizePacks.Add(pack);
|
sizePacks.Add(pack);
|
||||||
|
|
||||||
// Determine external connections; these are only valid when adjacent to a room node.
|
|
||||||
// We use this later to determine which room packs connect to each other
|
|
||||||
var nodes = new HashSet<Vector2i>();
|
|
||||||
externalNodes.Add(pack, nodes);
|
|
||||||
|
|
||||||
foreach (var room in pack.Rooms)
|
|
||||||
{
|
|
||||||
var rator = new Box2iEdgeEnumerator(room, false);
|
|
||||||
|
|
||||||
while (rator.MoveNext(out var index))
|
|
||||||
{
|
|
||||||
nodes.Add(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to sort to make the RNG deterministic (at least without prototype changes).
|
// Need to sort to make the RNG deterministic (at least without prototype changes).
|
||||||
@@ -52,7 +36,7 @@ public sealed partial class DungeonJob
|
|||||||
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
|
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomProtos = new Dictionary<Vector2i, List<DungeonRoomPrototype>>();
|
var roomProtos = new Dictionary<Vector2i, List<DungeonRoomPrototype>>(_prototype.Count<DungeonRoomPrototype>());
|
||||||
|
|
||||||
foreach (var proto in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
|
foreach (var proto in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
|
||||||
{
|
{
|
||||||
@@ -80,60 +64,13 @@ public sealed partial class DungeonJob
|
|||||||
roomA.Sort((x, y) =>
|
roomA.Sort((x, y) =>
|
||||||
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
|
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
// First we gather all of the edges for each roompack in the preset
|
|
||||||
// This allows us to determine which ones should connect from being adjacent
|
|
||||||
var edges = new HashSet<Vector2i>[gen.RoomPacks.Count];
|
|
||||||
|
|
||||||
for (var i = 0; i < gen.RoomPacks.Count; i++)
|
|
||||||
{
|
|
||||||
var pack = gen.RoomPacks[i];
|
|
||||||
var nodes = new HashSet<Vector2i>(pack.Width + 2 + pack.Height);
|
|
||||||
|
|
||||||
var rator = new Box2iEdgeEnumerator(pack, false);
|
|
||||||
|
|
||||||
while (rator.MoveNext(out var index))
|
|
||||||
{
|
|
||||||
nodes.Add(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
edges[i] = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build up edge groups between each pack.
|
|
||||||
var connections = new Dictionary<int, Dictionary<int, HashSet<Vector2i>>>();
|
|
||||||
|
|
||||||
for (var i = 0; i < edges.Length; i++)
|
|
||||||
{
|
|
||||||
var nodes = edges[i];
|
|
||||||
var nodeConnections = connections.GetOrNew(i);
|
|
||||||
|
|
||||||
for (var j = i + 1; j < edges.Length; j++)
|
|
||||||
{
|
|
||||||
var otherNodes = edges[j];
|
|
||||||
var intersect = new HashSet<Vector2i>(nodes);
|
|
||||||
|
|
||||||
intersect.IntersectWith(otherNodes);
|
|
||||||
|
|
||||||
if (intersect.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
nodeConnections[j] = intersect;
|
|
||||||
var otherNodeConnections = connections.GetOrNew(j);
|
|
||||||
otherNodeConnections[i] = intersect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tiles = new List<(Vector2i, Tile)>();
|
var tiles = new List<(Vector2i, Tile)>();
|
||||||
var dungeon = new Dungeon()
|
var dungeon = new Dungeon();
|
||||||
{
|
|
||||||
Position = _position
|
|
||||||
};
|
|
||||||
var availablePacks = new List<DungeonRoomPackPrototype>();
|
var availablePacks = new List<DungeonRoomPackPrototype>();
|
||||||
var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
|
var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
|
||||||
var packTransforms = new Matrix3[gen.RoomPacks.Count];
|
var packTransforms = new Matrix3[gen.RoomPacks.Count];
|
||||||
var packRotations = new Angle[gen.RoomPacks.Count];
|
var packRotations = new Angle[gen.RoomPacks.Count];
|
||||||
var rotatedPackNodes = new HashSet<Vector2i>[gen.RoomPacks.Count];
|
|
||||||
|
|
||||||
// Actually pick the room packs and rooms
|
// Actually pick the room packs and rooms
|
||||||
for (var i = 0; i < gen.RoomPacks.Count; i++)
|
for (var i = 0; i < gen.RoomPacks.Count; i++)
|
||||||
@@ -159,9 +96,6 @@ public sealed partial class DungeonJob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterate every pack
|
// Iterate every pack
|
||||||
// To be valid it needs its edge nodes to overlap with every edge group
|
|
||||||
var external = connections[i];
|
|
||||||
|
|
||||||
random.Shuffle(availablePacks);
|
random.Shuffle(availablePacks);
|
||||||
Matrix3 packTransform = default!;
|
Matrix3 packTransform = default!;
|
||||||
var found = false;
|
var found = false;
|
||||||
@@ -169,11 +103,12 @@ public sealed partial class DungeonJob
|
|||||||
|
|
||||||
foreach (var aPack in availablePacks)
|
foreach (var aPack in availablePacks)
|
||||||
{
|
{
|
||||||
var aExternal = externalNodes[aPack];
|
var startIndex = random.Next(4);
|
||||||
|
|
||||||
for (var j = 0; j < 4; j++)
|
for (var j = 0; j < 4; j++)
|
||||||
{
|
{
|
||||||
var dir = (DirectionFlag) Math.Pow(2, j);
|
var index = (startIndex + j) % 4;
|
||||||
|
var dir = (DirectionFlag) Math.Pow(2, index);
|
||||||
Vector2i aPackDimensions;
|
Vector2i aPackDimensions;
|
||||||
|
|
||||||
if ((dir & (DirectionFlag.East | DirectionFlag.West)) != 0x0)
|
if ((dir & (DirectionFlag.East | DirectionFlag.West)) != 0x0)
|
||||||
@@ -190,37 +125,11 @@ public sealed partial class DungeonJob
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
found = true;
|
found = true;
|
||||||
var rotatedNodes = new HashSet<Vector2i>(aExternal.Count);
|
|
||||||
var aRotation = dir.AsDir().ToAngle();
|
var aRotation = dir.AsDir().ToAngle();
|
||||||
|
|
||||||
// Get the external nodes in terms of the dungeon layout
|
|
||||||
// (i.e. rotated if necessary + translated to the room position)
|
|
||||||
foreach (var node in aExternal)
|
|
||||||
{
|
|
||||||
// Get the node in pack terms (offset from center), then rotate it
|
|
||||||
// Afterwards we offset it by where the pack is supposed to be in world terms.
|
|
||||||
var rotated = aRotation.RotateVec((Vector2) node + grid.TileSize / 2f - aPack.Size / 2f);
|
|
||||||
rotatedNodes.Add((rotated + bounds.Center).Floored());
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var group in external.Values)
|
|
||||||
{
|
|
||||||
if (rotatedNodes.Overlaps(group))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
found = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use this pack
|
// Use this pack
|
||||||
packTransform = Matrix3.CreateTransform(bounds.Center, aRotation);
|
packTransform = Matrix3.CreateTransform(bounds.Center, aRotation);
|
||||||
packRotations[i] = aRotation;
|
packRotations[i] = aRotation;
|
||||||
rotatedPackNodes[i] = rotatedNodes;
|
|
||||||
pack = aPack;
|
pack = aPack;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -311,6 +220,9 @@ public sealed partial class DungeonJob
|
|||||||
var templateGrid = _entManager.GetComponent<MapGridComponent>(templateMapUid);
|
var templateGrid = _entManager.GetComponent<MapGridComponent>(templateMapUid);
|
||||||
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 tileOffset = -roomCenter + grid.TileSize / 2f;
|
||||||
|
Box2i? mapBounds = null;
|
||||||
|
|
||||||
// Load tiles
|
// Load tiles
|
||||||
for (var x = 0; x < room.Size.X; x++)
|
for (var x = 0; x < room.Size.X; x++)
|
||||||
@@ -320,23 +232,42 @@ public sealed partial class DungeonJob
|
|||||||
var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
|
var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
|
||||||
var tileRef = templateGrid.GetTileRef(indices);
|
var tileRef = templateGrid.GetTileRef(indices);
|
||||||
|
|
||||||
var tilePos = dungeonMatty.Transform((Vector2) indices + grid.TileSize / 2f - roomCenter);
|
var tilePos = dungeonMatty.Transform(indices + tileOffset);
|
||||||
var rounded = tilePos.Floored();
|
var rounded = tilePos.Floored();
|
||||||
tiles.Add((rounded, tileRef.Tile));
|
tiles.Add((rounded, tileRef.Tile));
|
||||||
roomTiles.Add(rounded);
|
roomTiles.Add(rounded);
|
||||||
|
|
||||||
|
// If this were a Box2 we'd add tilesize although here I think that's undesirable as
|
||||||
|
// for example, a box2i of 0,0,1,1 is assumed to also include the tile at 1,1
|
||||||
|
mapBounds = mapBounds?.Union(new Box2i(rounded, rounded)) ?? new Box2i(rounded, rounded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var x = -1; x <= room.Size.X; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= room.Size.Y; y++)
|
||||||
|
{
|
||||||
|
if (x != -1 && y != -1 && x != room.Size.X && y != room.Size.Y)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tilePos = dungeonMatty.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset);
|
||||||
|
exterior.Add(tilePos.Floored());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = new Box2(room.Offset, room.Offset + room.Size);
|
||||||
var center = Vector2.Zero;
|
var center = Vector2.Zero;
|
||||||
|
|
||||||
foreach (var tile in roomTiles)
|
foreach (var tile in roomTiles)
|
||||||
{
|
{
|
||||||
center += ((Vector2) tile + grid.TileSize / 2f);
|
center += (Vector2) tile + grid.TileSize / 2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
center /= roomTiles.Count;
|
center /= roomTiles.Count;
|
||||||
|
|
||||||
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center));
|
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
|
||||||
grid.SetTiles(tiles);
|
grid.SetTiles(tiles);
|
||||||
tiles.Clear();
|
tiles.Clear();
|
||||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||||
@@ -344,7 +275,6 @@ public sealed partial class DungeonJob
|
|||||||
|
|
||||||
// Load entities
|
// Load entities
|
||||||
// TODO: I don't think engine supports full entity copying so we do this piece of shit.
|
// TODO: I don't think engine supports full entity copying so we do this piece of shit.
|
||||||
var bounds = new Box2(room.Offset, room.Offset + room.Size);
|
|
||||||
|
|
||||||
foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
|
foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
|
||||||
{
|
{
|
||||||
@@ -391,7 +321,7 @@ public sealed partial class DungeonJob
|
|||||||
{
|
{
|
||||||
position += new Vector2(-1f / 32f, 1f / 32f);
|
position += new Vector2(-1f / 32f, 1f / 32f);
|
||||||
}
|
}
|
||||||
else if (angle.Equals(Math.PI * 1.5))
|
else if (angle.Equals(-Math.PI / 2f))
|
||||||
{
|
{
|
||||||
position += new Vector2(-1f / 32f, 0f);
|
position += new Vector2(-1f / 32f, 0f);
|
||||||
}
|
}
|
||||||
@@ -399,6 +329,17 @@ public sealed partial class DungeonJob
|
|||||||
{
|
{
|
||||||
position += new Vector2(0f, 1f / 32f);
|
position += new Vector2(0f, 1f / 32f);
|
||||||
}
|
}
|
||||||
|
else if (angle.Equals(Math.PI * 1.5f))
|
||||||
|
{
|
||||||
|
// I hate this but decals are bottom-left rather than center position and doing the
|
||||||
|
// matrix ops is a PITA hence this workaround for now; I also don't want to add a stupid
|
||||||
|
// field for 1 specific op on decals
|
||||||
|
if (decal.Id != "DiagonalCheckerAOverlay" &&
|
||||||
|
decal.Id != "DiagonalCheckerBOverlay")
|
||||||
|
{
|
||||||
|
position += new Vector2(-1f / 32f, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var tilePos = position.Floored();
|
var tilePos = position.Floored();
|
||||||
|
|
||||||
@@ -427,16 +368,70 @@ public sealed partial class DungeonJob
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate center
|
// Calculate center and do entrances
|
||||||
var dungeonCenter = Vector2.Zero;
|
var dungeonCenter = Vector2.Zero;
|
||||||
|
|
||||||
foreach (var room in dungeon.Rooms)
|
foreach (var room in dungeon.Rooms)
|
||||||
{
|
{
|
||||||
dungeonCenter += room.Center;
|
dungeon.RoomTiles.UnionWith(room.Tiles);
|
||||||
|
dungeon.RoomExteriorTiles.UnionWith(room.Exterior);
|
||||||
}
|
}
|
||||||
|
|
||||||
dungeon.Center = (Vector2i) (dungeonCenter / dungeon.Rooms.Count);
|
foreach (var room in dungeon.Rooms)
|
||||||
|
{
|
||||||
|
dungeonCenter += room.Center;
|
||||||
|
SetDungeonEntrance(dungeon, room, random);
|
||||||
|
}
|
||||||
|
|
||||||
return dungeon;
|
return dungeon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, Random random)
|
||||||
|
{
|
||||||
|
// TODO: Move to dungeonsystem.
|
||||||
|
|
||||||
|
// TODO: Look at markers and use that.
|
||||||
|
|
||||||
|
// Pick midpoints as fallback
|
||||||
|
if (room.Entrances.Count == 0)
|
||||||
|
{
|
||||||
|
var offset = random.Next(4);
|
||||||
|
|
||||||
|
// Pick an entrance that isn't taken.
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var dir = (Direction) ((i + offset) * 2 % 8);
|
||||||
|
Vector2i entrancePos;
|
||||||
|
|
||||||
|
switch (dir)
|
||||||
|
{
|
||||||
|
case Direction.East:
|
||||||
|
entrancePos = new Vector2i(room.Bounds.Right + 1, room.Bounds.Bottom + room.Bounds.Height / 2);
|
||||||
|
break;
|
||||||
|
case Direction.North:
|
||||||
|
entrancePos = new Vector2i(room.Bounds.Left + room.Bounds.Width / 2, room.Bounds.Top + 1);
|
||||||
|
break;
|
||||||
|
case Direction.West:
|
||||||
|
entrancePos = new Vector2i(room.Bounds.Left - 1, room.Bounds.Bottom + room.Bounds.Height / 2);
|
||||||
|
break;
|
||||||
|
case Direction.South:
|
||||||
|
entrancePos = new Vector2i(room.Bounds.Left + room.Bounds.Width / 2, room.Bounds.Bottom - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's not blocked
|
||||||
|
var blockPos = entrancePos + dir.ToIntVec() * 2;
|
||||||
|
|
||||||
|
if (i != 3 && dungeon.RoomTiles.Contains(blockPos))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.Entrances.Add(entrancePos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,12 @@ using Content.Server.Decals;
|
|||||||
using Content.Shared.Procedural;
|
using Content.Shared.Procedural;
|
||||||
using Content.Shared.Procedural.DungeonGenerators;
|
using Content.Shared.Procedural.DungeonGenerators;
|
||||||
using Content.Shared.Procedural.PostGeneration;
|
using Content.Shared.Procedural.PostGeneration;
|
||||||
|
using Content.Shared.Tag;
|
||||||
using Robust.Server.Physics;
|
using Robust.Server.Physics;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
private readonly DungeonSystem _dungeon;
|
private readonly DungeonSystem _dungeon;
|
||||||
private readonly EntityLookupSystem _lookup;
|
private readonly EntityLookupSystem _lookup;
|
||||||
private readonly SharedTransformSystem _transform;
|
private readonly SharedTransformSystem _transform;
|
||||||
|
private EntityQuery<TagComponent> _tagQuery;
|
||||||
|
|
||||||
private readonly DungeonConfigPrototype _gen;
|
private readonly DungeonConfigPrototype _gen;
|
||||||
private readonly int _seed;
|
private readonly int _seed;
|
||||||
@@ -65,6 +68,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
_dungeon = dungeon;
|
_dungeon = dungeon;
|
||||||
_lookup = lookup;
|
_lookup = lookup;
|
||||||
_transform = transform;
|
_transform = transform;
|
||||||
|
_tagQuery = _entManager.GetEntityQuery<TagComponent>();
|
||||||
|
|
||||||
_gen = gen;
|
_gen = gen;
|
||||||
_grid = grid;
|
_grid = grid;
|
||||||
@@ -88,10 +92,8 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var room in dungeon.Rooms)
|
DebugTools.Assert(dungeon.RoomTiles.Count > 0);
|
||||||
{
|
DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
|
||||||
dungeon.RoomTiles.UnionWith(room.Tiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// To make it slightly more deterministic treat this RNG as separate ig.
|
// To make it slightly more deterministic treat this RNG as separate ig.
|
||||||
var random = new Random(_seed);
|
var random = new Random(_seed);
|
||||||
@@ -102,10 +104,31 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
|
|
||||||
switch (post)
|
switch (post)
|
||||||
{
|
{
|
||||||
|
case AutoCablingPostGen cabling:
|
||||||
|
await PostGen(cabling, 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 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:
|
case MiddleConnectionPostGen dordor:
|
||||||
await PostGen(dordor, dungeon, _gridUid, _grid, random);
|
await PostGen(dordor, dungeon, _gridUid, _grid, random);
|
||||||
break;
|
break;
|
||||||
case EntrancePostGen entrance:
|
case DungeonEntrancePostGen entrance:
|
||||||
await PostGen(entrance, dungeon, _gridUid, _grid, random);
|
await PostGen(entrance, dungeon, _gridUid, _grid, random);
|
||||||
break;
|
break;
|
||||||
case ExternalWindowPostGen externalWindow:
|
case ExternalWindowPostGen externalWindow:
|
||||||
@@ -114,8 +137,8 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
case InternalWindowPostGen internalWindow:
|
case InternalWindowPostGen internalWindow:
|
||||||
await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
|
await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
|
||||||
break;
|
break;
|
||||||
case BoundaryWallPostGen boundary:
|
case RoomEntrancePostGen rEntrance:
|
||||||
await PostGen(boundary, dungeon, _gridUid, _grid, random);
|
await PostGen(rEntrance, dungeon, _gridUid, _grid, random);
|
||||||
break;
|
break;
|
||||||
case WallMountPostGen wall:
|
case WallMountPostGen wall:
|
||||||
await PostGen(wall, dungeon, _gridUid, _grid, random);
|
await PostGen(wall, dungeon, _gridUid, _grid, random);
|
||||||
@@ -125,7 +148,9 @@ public sealed partial class DungeonJob : Job<Dungeon>
|
|||||||
}
|
}
|
||||||
|
|
||||||
await SuspendIfOutOfTime();
|
await SuspendIfOutOfTime();
|
||||||
ValidateResume();
|
|
||||||
|
if (!ValidateResume())
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_grid.CanSplit = true;
|
_grid.CanSplit = true;
|
||||||
|
|||||||
196
Content.Server/Procedural/DungeonSystem.Helpers.cs
Normal file
196
Content.Server/Procedural/DungeonSystem.Helpers.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using Content.Shared.NPC;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Procedural;
|
||||||
|
|
||||||
|
public sealed partial class DungeonSystem
|
||||||
|
{
|
||||||
|
public List<(Vector2i Start, Vector2i End)> MinimumSpanningTree(List<Vector2i> tiles, System.Random random)
|
||||||
|
{
|
||||||
|
// Generate connections between all rooms.
|
||||||
|
var connections = new Dictionary<Vector2i, List<(Vector2i Tile, float Distance)>>(tiles.Count);
|
||||||
|
|
||||||
|
foreach (var entrance in tiles)
|
||||||
|
{
|
||||||
|
var edgeConns = new List<(Vector2i Tile, float Distance)>(tiles.Count - 1);
|
||||||
|
|
||||||
|
foreach (var other in tiles)
|
||||||
|
{
|
||||||
|
if (entrance == other)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
edgeConns.Add((other, (other - entrance).Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort these as they will be iterated many times.
|
||||||
|
edgeConns.Sort((x, y) => x.Distance.CompareTo(y.Distance));
|
||||||
|
connections.Add(entrance, edgeConns);
|
||||||
|
}
|
||||||
|
|
||||||
|
var seedIndex = random.Next(tiles.Count);
|
||||||
|
var remaining = new ValueList<Vector2i>(tiles);
|
||||||
|
remaining.RemoveAt(seedIndex);
|
||||||
|
|
||||||
|
var edges = new List<(Vector2i Start, Vector2i End)>();
|
||||||
|
|
||||||
|
var seedEntrance = tiles[seedIndex];
|
||||||
|
var forest = new ValueList<Vector2i>(tiles.Count) { seedEntrance };
|
||||||
|
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
// Get cheapest edge
|
||||||
|
var cheapestDistance = float.MaxValue;
|
||||||
|
var cheapest = (Vector2i.Zero, Vector2i.Zero);
|
||||||
|
|
||||||
|
foreach (var node in forest)
|
||||||
|
{
|
||||||
|
foreach (var conn in connections[node])
|
||||||
|
{
|
||||||
|
// Existing tile, skip
|
||||||
|
if (forest.Contains(conn.Tile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Not the cheapest
|
||||||
|
if (cheapestDistance < conn.Distance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cheapestDistance = conn.Distance;
|
||||||
|
cheapest = (node, conn.Tile);
|
||||||
|
// List is pre-sorted so we can just breakout easily.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTools.Assert(cheapestDistance < float.MaxValue);
|
||||||
|
// Add to tree
|
||||||
|
edges.Add(cheapest);
|
||||||
|
forest.Add(cheapest.Item2);
|
||||||
|
remaining.Remove(cheapest.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Primarily for dungeon usage.
|
||||||
|
/// </summary>
|
||||||
|
public void GetCorridorNodes(HashSet<Vector2i> corridorTiles,
|
||||||
|
List<(Vector2i Start, Vector2i End)> edges,
|
||||||
|
int pathLimit,
|
||||||
|
HashSet<Vector2i>? forbiddenTiles = null,
|
||||||
|
Func<Vector2i, float>? tileCallback = null)
|
||||||
|
{
|
||||||
|
// Pathfind each entrance
|
||||||
|
var frontier = new PriorityQueue<Vector2i, float>();
|
||||||
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
||||||
|
var directions = new Dictionary<Vector2i, Direction>();
|
||||||
|
var costSoFar = new Dictionary<Vector2i, float>();
|
||||||
|
forbiddenTiles ??= new HashSet<Vector2i>();
|
||||||
|
|
||||||
|
foreach (var (start, end) in edges)
|
||||||
|
{
|
||||||
|
frontier.Clear();
|
||||||
|
cameFrom.Clear();
|
||||||
|
costSoFar.Clear();
|
||||||
|
directions.Clear();
|
||||||
|
directions[start] = Direction.Invalid;
|
||||||
|
frontier.Enqueue(start, 0f);
|
||||||
|
costSoFar[start] = 0f;
|
||||||
|
var found = false;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (frontier.Count > 0 && count < pathLimit)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
var node = frontier.Dequeue();
|
||||||
|
|
||||||
|
if (node == end)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastDirection = directions[node];
|
||||||
|
|
||||||
|
// Foreach neighbor etc etc
|
||||||
|
for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 1; y++)
|
||||||
|
{
|
||||||
|
// Cardinals only.
|
||||||
|
if (x != 0 && y != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var neighbor = new Vector2i(node.X + x, node.Y + y);
|
||||||
|
|
||||||
|
// FORBIDDEN
|
||||||
|
if (neighbor != end &&
|
||||||
|
forbiddenTiles.Contains(neighbor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileCost = SharedPathfindingSystem.ManhattanDistance(node, neighbor);
|
||||||
|
|
||||||
|
// Weight towards existing corridors ig
|
||||||
|
if (corridorTiles.Contains(neighbor))
|
||||||
|
{
|
||||||
|
tileCost *= 0.10f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var costMod = tileCallback?.Invoke(neighbor);
|
||||||
|
costMod ??= 1f;
|
||||||
|
tileCost *= costMod.Value;
|
||||||
|
|
||||||
|
var direction = (neighbor - node).GetCardinalDir();
|
||||||
|
directions[neighbor] = direction;
|
||||||
|
|
||||||
|
// If direction is different then penalise it.
|
||||||
|
if (direction != lastDirection)
|
||||||
|
{
|
||||||
|
tileCost *= 3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f = g + h
|
||||||
|
// gScore is distance to the start node
|
||||||
|
// hScore is distance to the end node
|
||||||
|
var gScore = costSoFar[node] + tileCost;
|
||||||
|
|
||||||
|
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameFrom[neighbor] = node;
|
||||||
|
costSoFar[neighbor] = gScore;
|
||||||
|
|
||||||
|
// Make it greedy so multiply h-score to punish further nodes.
|
||||||
|
// This is necessary as we might have the deterredTiles multiplying towards the end
|
||||||
|
// so just finish it.
|
||||||
|
var hScore = SharedPathfindingSystem.ManhattanDistance(end, neighbor) * (1.0f - 1.0f / 1000.0f);
|
||||||
|
var fScore = gScore + hScore;
|
||||||
|
frontier.Enqueue(neighbor, fScore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild path if it's valid.
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
var node = end;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
node = cameFrom[node];
|
||||||
|
|
||||||
|
// Don't want start or end nodes included.
|
||||||
|
if (node == start)
|
||||||
|
break;
|
||||||
|
|
||||||
|
corridorTiles.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ using Robust.Shared.Prototypes;
|
|||||||
|
|
||||||
namespace Content.Server.Procedural;
|
namespace Content.Server.Procedural;
|
||||||
|
|
||||||
public sealed partial class DungeonSystem : EntitySystem
|
public sealed partial class DungeonSystem : SharedDungeonSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||||
[Dependency] private readonly IConsoleHost _console = default!;
|
[Dependency] private readonly IConsoleHost _console = default!;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class StatValuesCommand : IConsoleCommand
|
|||||||
|
|
||||||
public string Command => "showvalues";
|
public string Command => "showvalues";
|
||||||
public string Description => Loc.GetString("stat-values-desc");
|
public string Description => Loc.GetString("stat-values-desc");
|
||||||
public string Help => $"{Command} <cargosell / lathsell / melee>";
|
public string Help => $"{Command} <cargosell / lathesell / melee>";
|
||||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
{
|
{
|
||||||
if (shell.Player is not IPlayerSession pSession)
|
if (shell.Player is not IPlayerSession pSession)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class StaminaComponent : Component
|
|||||||
/// How much time after receiving damage until stamina starts decreasing.
|
/// How much time after receiving damage until stamina starts decreasing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
|
[ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
|
||||||
public float DecayCooldown = 5f;
|
public float DecayCooldown = 3f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much stamina damage this entity has taken.
|
/// How much stamina damage this entity has taken.
|
||||||
|
|||||||
@@ -19,4 +19,17 @@ public abstract class SharedPathfindingSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
return new Vector2(index.X, index.Y) / SubStep+ (chunk) * ChunkSize + StepOffset;
|
return new Vector2(index.X, index.Y) / SubStep+ (chunk) * ChunkSize + StepOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static float ManhattanDistance(Vector2i start, Vector2i end)
|
||||||
|
{
|
||||||
|
var distance = end - start;
|
||||||
|
return Math.Abs(distance.X) + Math.Abs(distance.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float OctileDistance(Vector2i start, Vector2i end)
|
||||||
|
{
|
||||||
|
var diff = start - end;
|
||||||
|
var ab = Vector2.Abs(diff);
|
||||||
|
return ab.X + ab.Y + (1.41f - 2) * Math.Min(ab.X, ab.Y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ namespace Content.Shared.Procedural;
|
|||||||
|
|
||||||
public sealed class Dungeon
|
public sealed class Dungeon
|
||||||
{
|
{
|
||||||
/// <summary>
|
public readonly List<DungeonRoom> Rooms = new();
|
||||||
/// Starting position used to generate the dungeon from.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2i Position;
|
|
||||||
|
|
||||||
public Vector2i Center;
|
|
||||||
|
|
||||||
public List<DungeonRoom> Rooms = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hashset of the tiles across all rooms.
|
/// Hashset of the tiles across all rooms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<Vector2i> RoomTiles = new();
|
public readonly HashSet<Vector2i> RoomTiles = new();
|
||||||
|
|
||||||
|
public readonly HashSet<Vector2i> RoomExteriorTiles = new();
|
||||||
|
|
||||||
|
public readonly HashSet<Vector2i> CorridorTiles = new();
|
||||||
|
|
||||||
|
public readonly HashSet<Vector2i> CorridorExteriorTiles = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
namespace Content.Shared.Procedural;
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center);
|
public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior)
|
||||||
|
{
|
||||||
|
public List<Vector2i> Entrances = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nodes adjacent to tiles, including the corners.
|
||||||
|
/// </summary>
|
||||||
|
public readonly HashSet<Vector2i> Exterior = Exterior;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs cables throughout the dungeon.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AutoCablingPostGen : IPostDunGen
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Content.Shared.Storage;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns entities inside corners.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CornerClutterPostGen : IPostDunGen
|
||||||
|
{
|
||||||
|
[DataField("chance")]
|
||||||
|
public float Chance = 0.50f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default starting bulbs
|
||||||
|
/// </summary>
|
||||||
|
[DataField("contents", required: true)]
|
||||||
|
public List<EntitySpawnEntry> Contents = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Content.Shared.Decals;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||||
|
|
||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies decal skirting to corridors.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CorridorDecalSkirtingPostGen : IPostDunGen
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Color to apply to decals.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("color")]
|
||||||
|
public Color? Color;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decal where 1 edge is found.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("cardinalDecals")]
|
||||||
|
public Dictionary<DirectionFlag, string> CardinalDecals = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decal where 1 corner edge is found.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("pocketDecals")]
|
||||||
|
public Dictionary<Direction, string> PocketDecals = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decal where 2 or 3 edges are found.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("cornerDecals")]
|
||||||
|
public Dictionary<DirectionFlag, string> CornerDecals = new();
|
||||||
|
}
|
||||||
31
Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs
Normal file
31
Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
namespace Content.Shared.Procedural.PostGeneration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects room entrances via corridor segments.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CorridorPostGen : IPostDunGen
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How far we're allowed to generate a corridor before calling it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Given the heavy weightings this needs to be fairly large for larger dungeons.
|
||||||
|
/// </remarks>
|
||||||
|
[DataField("pathLimit")]
|
||||||
|
public int PathLimit = 2048;
|
||||||
|
|
||||||
|
[DataField("method")]
|
||||||
|
public CorridorPostGenMethod Method = CorridorPostGenMethod.MinimumSpanningTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How wide to make the corridor.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("width")]
|
||||||
|
public int Width = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CorridorPostGenMethod : byte
|
||||||
|
{
|
||||||
|
Invalid,
|
||||||
|
MinimumSpanningTree,
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects [count] rooms and places external doors to them.
|
/// Selects [count] rooms and places external doors to them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EntrancePostGen : IPostDunGen
|
public sealed class DungeonEntrancePostGen : IPostDunGen
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many rooms we place doors on.
|
/// How many rooms we place doors on.
|
||||||
@@ -17,9 +17,10 @@ public sealed class EntrancePostGen : IPostDunGen
|
|||||||
public int Count = 1;
|
public int Count = 1;
|
||||||
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||||
public List<string> Entities = new()
|
public List<string?> Entities = new()
|
||||||
{
|
{
|
||||||
"AirlockGlass"
|
"CableApcExtension",
|
||||||
|
"AirlockGlass",
|
||||||
};
|
};
|
||||||
|
|
||||||
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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 class EntranceFlankPostGen : IPostDunGen
|
||||||
|
{
|
||||||
|
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
||||||
|
public string Tile = "FloorSteel";
|
||||||
|
|
||||||
|
[DataField("entities")]
|
||||||
|
public List<string?> Entities = new();
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
public sealed class ExternalWindowPostGen : IPostDunGen
|
public sealed class ExternalWindowPostGen : IPostDunGen
|
||||||
{
|
{
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||||
public List<string> Entities = new()
|
public List<string?> Entities = new()
|
||||||
{
|
{
|
||||||
"Grille",
|
"Grille",
|
||||||
"Window",
|
"Window",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Content.Shared.Procedural.PostGeneration;
|
|||||||
public sealed class InternalWindowPostGen : IPostDunGen
|
public sealed class InternalWindowPostGen : IPostDunGen
|
||||||
{
|
{
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||||
public List<string> Entities = new()
|
public List<string?> Entities = new()
|
||||||
{
|
{
|
||||||
"Grille",
|
"Grille",
|
||||||
"Window",
|
"Window",
|
||||||
|
|||||||
28
Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs
Normal file
28
Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ public sealed class MiddleConnectionPostGen : IPostDunGen
|
|||||||
public string Tile = "FloorSteel";
|
public string Tile = "FloorSteel";
|
||||||
|
|
||||||
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||||
public List<string> Entities = new()
|
public List<string?> Entities = new()
|
||||||
{
|
{
|
||||||
"CableApcExtension",
|
"CableApcExtension",
|
||||||
"AirlockGlass"
|
"AirlockGlass"
|
||||||
@@ -35,5 +35,5 @@ public sealed class MiddleConnectionPostGen : IPostDunGen
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// If overlap > 1 then what should spawn on the edges.
|
/// If overlap > 1 then what should spawn on the edges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("edgeEntities")] public List<string>? EdgeEntities;
|
[DataField("edgeEntities")] public List<string?> EdgeEntities = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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 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";
|
||||||
|
}
|
||||||
6
Content.Shared/Procedural/SharedDungeonSystem.cs
Normal file
6
Content.Shared/Procedural/SharedDungeonSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Content.Shared.Procedural;
|
||||||
|
|
||||||
|
public abstract class SharedDungeonSystem : EntitySystem
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,32 +4,23 @@
|
|||||||
roomWhitelist:
|
roomWhitelist:
|
||||||
- SalvageExperiment
|
- SalvageExperiment
|
||||||
presets:
|
presets:
|
||||||
- Cross
|
- Bucket
|
||||||
- SpaceMan
|
- Wow
|
||||||
- FourSquare
|
- SpaceShip
|
||||||
|
- Tall
|
||||||
postGeneration:
|
postGeneration:
|
||||||
- !type:MiddleConnectionPostGen
|
- !type:CorridorPostGen
|
||||||
overlapCount: 3
|
width: 3
|
||||||
count: 3
|
|
||||||
entities:
|
|
||||||
- CableApcExtension
|
|
||||||
- AirlockGlass
|
|
||||||
edgeEntities:
|
|
||||||
- Grille
|
|
||||||
- Window
|
|
||||||
|
|
||||||
- !type:MiddleConnectionPostGen
|
- !type:DungeonEntrancePostGen
|
||||||
count: 1
|
|
||||||
entities:
|
|
||||||
- CableApcExtension
|
|
||||||
- AirlockGlass
|
|
||||||
|
|
||||||
- !type:EntrancePostGen
|
|
||||||
count: 2
|
count: 2
|
||||||
|
|
||||||
|
- !type:RoomEntrancePostGen
|
||||||
entities:
|
entities:
|
||||||
|
- CableApcExtension
|
||||||
- AirlockGlass
|
- AirlockGlass
|
||||||
|
|
||||||
- !type:InternalWindowPostGen
|
- !type:EntranceFlankPostGen
|
||||||
entities:
|
entities:
|
||||||
- Grille
|
- Grille
|
||||||
- Window
|
- Window
|
||||||
@@ -59,38 +50,60 @@
|
|||||||
wall: WallSolid
|
wall: WallSolid
|
||||||
cornerWall: WallReinforced
|
cornerWall: WallReinforced
|
||||||
|
|
||||||
|
- !type:JunctionPostGen
|
||||||
|
width: 1
|
||||||
|
|
||||||
|
- !type:JunctionPostGen
|
||||||
|
|
||||||
|
- !type:AutoCablingPostGen
|
||||||
|
|
||||||
|
- !type:CornerClutterPostGen
|
||||||
|
contents:
|
||||||
|
- id: PottedPlantRandom
|
||||||
|
amount: 1
|
||||||
|
|
||||||
|
- !type:CorridorDecalSkirtingPostGen
|
||||||
|
color: "#D381C996"
|
||||||
|
cardinalDecals:
|
||||||
|
South: BrickTileWhiteLineS
|
||||||
|
East: BrickTileWhiteLineE
|
||||||
|
North: BrickTileWhiteLineN
|
||||||
|
West: BrickTileWhiteLineW
|
||||||
|
cornerDecals:
|
||||||
|
SouthEast: BrickTileWhiteCornerSe
|
||||||
|
SouthWest: BrickTileWhiteCornerSw
|
||||||
|
NorthEast: BrickTileWhiteCornerNe
|
||||||
|
NorthWest: BrickTileWhiteCornerNw
|
||||||
|
pocketDecals:
|
||||||
|
SouthWest: BrickTileWhiteInnerSw
|
||||||
|
SouthEast: BrickTileWhiteInnerSe
|
||||||
|
NorthWest: BrickTileWhiteInnerNw
|
||||||
|
NorthEast: BrickTileWhiteInnerNe
|
||||||
|
|
||||||
|
|
||||||
- type: dungeonConfig
|
- type: dungeonConfig
|
||||||
id: LavaBrig
|
id: LavaBrig
|
||||||
generator: !type:PrefabDunGen
|
generator: !type:PrefabDunGen
|
||||||
roomWhitelist:
|
roomWhitelist:
|
||||||
- LavaBrig
|
- LavaBrig
|
||||||
presets:
|
presets:
|
||||||
- Cross
|
- Bucket
|
||||||
- SpaceMan
|
- Wow
|
||||||
- FourSquare
|
- SpaceShip
|
||||||
|
- Tall
|
||||||
postGeneration:
|
postGeneration:
|
||||||
- !type:MiddleConnectionPostGen
|
- !type:CorridorPostGen
|
||||||
overlapCount: 3
|
width: 3
|
||||||
count: 3
|
|
||||||
entities:
|
|
||||||
- CableApcExtension
|
|
||||||
- AirlockSecurityGlassLocked
|
|
||||||
edgeEntities:
|
|
||||||
- Grille
|
|
||||||
- Window
|
|
||||||
|
|
||||||
- !type:MiddleConnectionPostGen
|
- !type:DungeonEntrancePostGen
|
||||||
count: 1
|
|
||||||
entities:
|
|
||||||
- CableApcExtension
|
|
||||||
- AirlockSecurityGlassLocked
|
|
||||||
|
|
||||||
- !type:EntrancePostGen
|
|
||||||
count: 2
|
count: 2
|
||||||
|
|
||||||
|
- !type:RoomEntrancePostGen
|
||||||
entities:
|
entities:
|
||||||
|
- CableApcExtension
|
||||||
- AirlockSecurityGlassLocked
|
- AirlockSecurityGlassLocked
|
||||||
|
|
||||||
- !type:InternalWindowPostGen
|
- !type:EntranceFlankPostGen
|
||||||
entities:
|
entities:
|
||||||
- Grille
|
- Grille
|
||||||
- Window
|
- Window
|
||||||
@@ -98,7 +111,7 @@
|
|||||||
- !type:ExternalWindowPostGen
|
- !type:ExternalWindowPostGen
|
||||||
entities:
|
entities:
|
||||||
- Grille
|
- Grille
|
||||||
- ReinforcedWindow
|
- Window
|
||||||
|
|
||||||
- !type:WallMountPostGen
|
- !type:WallMountPostGen
|
||||||
spawns:
|
spawns:
|
||||||
@@ -117,5 +130,35 @@
|
|||||||
|
|
||||||
- !type:BoundaryWallPostGen
|
- !type:BoundaryWallPostGen
|
||||||
tile: FloorSteel
|
tile: FloorSteel
|
||||||
wall: WallReinforced
|
wall: WallSolid
|
||||||
cornerWall: WallReinforced
|
cornerWall: WallReinforced
|
||||||
|
|
||||||
|
- !type:JunctionPostGen
|
||||||
|
width: 1
|
||||||
|
|
||||||
|
- !type:JunctionPostGen
|
||||||
|
|
||||||
|
- !type:AutoCablingPostGen
|
||||||
|
|
||||||
|
- !type:CornerClutterPostGen
|
||||||
|
contents:
|
||||||
|
- id: PottedPlantRandom
|
||||||
|
amount: 1
|
||||||
|
|
||||||
|
- !type:CorridorDecalSkirtingPostGen
|
||||||
|
color: "#DE3A3A96"
|
||||||
|
cardinalDecals:
|
||||||
|
South: BrickTileWhiteLineS
|
||||||
|
East: BrickTileWhiteLineE
|
||||||
|
North: BrickTileWhiteLineN
|
||||||
|
West: BrickTileWhiteLineW
|
||||||
|
cornerDecals:
|
||||||
|
SouthEast: BrickTileWhiteCornerSe
|
||||||
|
SouthWest: BrickTileWhiteCornerSw
|
||||||
|
NorthEast: BrickTileWhiteCornerNe
|
||||||
|
NorthWest: BrickTileWhiteCornerNw
|
||||||
|
pocketDecals:
|
||||||
|
SouthWest: BrickTileWhiteInnerSw
|
||||||
|
SouthEast: BrickTileWhiteInnerSe
|
||||||
|
NorthWest: BrickTileWhiteInnerNw
|
||||||
|
NorthEast: BrickTileWhiteInnerNe
|
||||||
|
|||||||
@@ -1,37 +1,35 @@
|
|||||||
# Dungeon presets
|
|
||||||
- type: dungeonPreset
|
- type: dungeonPreset
|
||||||
id: Cross
|
id: Bucket
|
||||||
roomPacks:
|
roomPacks:
|
||||||
- -8,0,9,5
|
- 0,0,17,17
|
||||||
- -2,6,3,11
|
- 20,0,37,17
|
||||||
# Offset to the first one
|
- 20,20,37,37
|
||||||
- -8,12,9,17
|
- -20,0,-3,17
|
||||||
- -2,18,3,35
|
- -20,20,-3,37
|
||||||
- -2,36,3,53
|
|
||||||
- -20,18,-3,35
|
|
||||||
- 4,18,21,35
|
|
||||||
|
|
||||||
# Two stumpy legs at the bottom, middle torso, then fat top
|
|
||||||
- type: dungeonPreset
|
|
||||||
id: SpaceMan
|
|
||||||
roomPacks:
|
|
||||||
- -14,0,-9,17
|
|
||||||
- -8,12,9,17
|
|
||||||
- 10,0,15,17
|
|
||||||
- -8,18,-3,23
|
|
||||||
- 4,18,9,23
|
|
||||||
- -2,18,3,35
|
|
||||||
- -8,36,9,53
|
|
||||||
- -14,36,-9,53
|
|
||||||
- 10,36,15,53
|
|
||||||
|
|
||||||
- type: dungeonPreset
|
- type: dungeonPreset
|
||||||
id: FourSquare
|
id: SpaceShip
|
||||||
roomPacks:
|
roomPacks:
|
||||||
- -38,18,-21,35
|
- 0,10,17,27
|
||||||
- -8,36,9,53
|
- 20,0,37,17
|
||||||
- 22,18,39,35
|
- 20,20,37,37
|
||||||
- -8,0,9,17
|
- -20,0,-3,17
|
||||||
- -2,18,3,35
|
- -20,20,-3,37
|
||||||
- -20,24,-3,29
|
|
||||||
- 4,24,21,29
|
- type: dungeonPreset
|
||||||
|
id: Tall
|
||||||
|
roomPacks:
|
||||||
|
- 0,0,17,17
|
||||||
|
- 0,20,17,37
|
||||||
|
- 20,37,37,54
|
||||||
|
- 0,54,17,71
|
||||||
|
- 0,74,17,91
|
||||||
|
|
||||||
|
- type: dungeonPreset
|
||||||
|
id: Wow
|
||||||
|
roomPacks:
|
||||||
|
- 0,20,17,37
|
||||||
|
- 20,0,37,17
|
||||||
|
- 40,20,57,37
|
||||||
|
- -20,0,-3,17
|
||||||
|
- -40,20,-23,37
|
||||||
|
|||||||
@@ -1,132 +1,43 @@
|
|||||||
# Hook
|
# 1341158413 seed cooked top 2 rooms are fucked
|
||||||
|
# 17x5 Y
|
||||||
|
# 11x5 Y
|
||||||
|
# 7x5 YY
|
||||||
|
# 5x5 YY
|
||||||
|
# 3x5 YY
|
||||||
|
|
||||||
|
# 13x3 YY
|
||||||
|
# 11x3 Y
|
||||||
|
# 7x3 Y
|
||||||
|
# 7x7 Y
|
||||||
|
|
||||||
|
- type: dungeonRoomPack
|
||||||
|
id: LargeArea0
|
||||||
|
size: 17,17
|
||||||
|
rooms:
|
||||||
|
- 0,0,5,17
|
||||||
|
- 10,10,17,17
|
||||||
|
- 12,0,17,3
|
||||||
|
|
||||||
|
- type: dungeonRoomPack
|
||||||
|
id: LargeArea1
|
||||||
|
size: 17,17
|
||||||
|
rooms:
|
||||||
|
- 1,1,6,12
|
||||||
|
- 11,1,16,6
|
||||||
|
- 9,13,16,16
|
||||||
|
|
||||||
- type: dungeonRoomPack
|
- type: dungeonRoomPack
|
||||||
id: LargeArea2
|
id: LargeArea2
|
||||||
size: 17,17
|
size: 17,17
|
||||||
rooms:
|
rooms:
|
||||||
- 7,0,10,11
|
- 1,2,8,7
|
||||||
- 6,12,11,17
|
- 1,10,8,15
|
||||||
- 12,13,17,16
|
- 13,2,16,15
|
||||||
|
|
||||||
# Wide corridor vertically up the middle and small corridors on left
|
|
||||||
- type: dungeonRoomPack
|
- type: dungeonRoomPack
|
||||||
id: LargeArea3
|
id: LargeArea3
|
||||||
size: 17,17
|
size: 17,17
|
||||||
rooms:
|
rooms:
|
||||||
- 6,0,11,11
|
- 2,3,5,8
|
||||||
- 6,12,11,17
|
- 1,10,6,15
|
||||||
- 0,13,5,16
|
- 13,2,16,15
|
||||||
- 1,5,4,12
|
|
||||||
- 0,1,5,4
|
|
||||||
|
|
||||||
# Long horizontal corridor with rooms above
|
|
||||||
#- type: dungeonRoomPack
|
|
||||||
# id: LargeArea4
|
|
||||||
# size: 17,17
|
|
||||||
# rooms:
|
|
||||||
# - 0,7,17,10
|
|
||||||
# - 0,11,5,16
|
|
||||||
# - 6,11,11,16
|
|
||||||
|
|
||||||
# Corridor from botleft to topright with 2 rooms in top left
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: LargeArea5
|
|
||||||
size: 17,17
|
|
||||||
rooms:
|
|
||||||
# Corridor (with fat bot-left)
|
|
||||||
- 0,1,11,4
|
|
||||||
- 12,0,17,5
|
|
||||||
- 13,6,16,17
|
|
||||||
# Rooms (5x7)
|
|
||||||
- 7,9,12,16
|
|
||||||
- 1,5,6,12
|
|
||||||
|
|
||||||
# 17x5 corridor through middle with 2 7x5 rooms off to the side.
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: LargeArea6
|
|
||||||
size: 17,17
|
|
||||||
rooms:
|
|
||||||
- 0,6,17,11
|
|
||||||
- 0,0,7,5
|
|
||||||
- 10,0,17,5
|
|
||||||
|
|
||||||
# 3x7 corridor leading to 7x7 room.
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: LargeArea7
|
|
||||||
size: 17,17
|
|
||||||
rooms:
|
|
||||||
- 0,7,7,10
|
|
||||||
- 8,5,15,12
|
|
||||||
|
|
||||||
# 17x5 corridor to 7x7
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: LargeArea8
|
|
||||||
size: 17,17
|
|
||||||
rooms:
|
|
||||||
- 0,1,17,6
|
|
||||||
- 5,7,12,14
|
|
||||||
|
|
||||||
# Medium
|
|
||||||
# Whole area room
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea1
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,17
|
|
||||||
|
|
||||||
# Three 5x5 rooms
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea2
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,5
|
|
||||||
- 0,6,5,11
|
|
||||||
- 0,12,5,17
|
|
||||||
|
|
||||||
# Two 5x5 and 3x5
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea3
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,5
|
|
||||||
- 0,6,5,11
|
|
||||||
- 1,12,4,17
|
|
||||||
|
|
||||||
# 3x5 -> 5x5 -> 3x5
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea4
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 1,0,4,5
|
|
||||||
- 0,6,5,11
|
|
||||||
- 1,12,4,17
|
|
||||||
|
|
||||||
# 3x5 then a 13x3
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea5
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,3
|
|
||||||
- 1,4,4,17
|
|
||||||
|
|
||||||
# 5x5 then a 11x3
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea6
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,5
|
|
||||||
- 1,6,4,17
|
|
||||||
|
|
||||||
# 5x5 then a 11x5
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: MediumArea7
|
|
||||||
size: 5,17
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,5
|
|
||||||
- 0,6,5,17
|
|
||||||
|
|
||||||
# Small
|
|
||||||
- type: dungeonRoomPack
|
|
||||||
id: SmallArea1
|
|
||||||
size: 5,5
|
|
||||||
rooms:
|
|
||||||
- 0,0,5,5
|
|
||||||
|
|||||||
Reference in New Issue
Block a user