Dungeon generation refactor (#17121)

This commit is contained in:
metalgearsloth
2023-06-27 19:17:42 +10:00
committed by GitHub
parent b3d395f214
commit cca1a78384
26 changed files with 1528 additions and 496 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View 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);
}
}
}
}
}

View File

@@ -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!;

View File

@@ -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)

View File

@@ -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.

View File

@@ -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);
}
} }

View File

@@ -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();
} }

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Runs cables throughout the dungeon.
/// </summary>
public sealed class AutoCablingPostGen : IPostDunGen
{
}

View File

@@ -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();
}

View File

@@ -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();
}

View 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,
}

View File

@@ -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>))]

View File

@@ -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();
}

View File

@@ -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",

View File

@@ -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",

View 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"
};
}

View File

@@ -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();
} }

View File

@@ -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";
}

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.Procedural;
public abstract class SharedDungeonSystem : EntitySystem
{
}

View File

@@ -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

View File

@@ -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

View File

@@ -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