diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs index f80cd61e9d..5d7ac4ad03 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs +++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Distance.cs @@ -16,25 +16,12 @@ public sealed partial class PathfindingSystem 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) { var (dx, dy) = GetDiff(start, end); 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) { var startPos = start.Box.Center; diff --git a/Content.Server/Procedural/DungeonJob.Generator.cs b/Content.Server/Procedural/DungeonJob.Generator.cs index 28f4eb5e4e..3c64eadfc2 100644 --- a/Content.Server/Procedural/DungeonJob.Generator.cs +++ b/Content.Server/Procedural/DungeonJob.Generator.cs @@ -20,7 +20,6 @@ public sealed partial class DungeonJob var dungeonRotation = _dungeon.GetDungeonRotation(seed); var dungeonTransform = Matrix3.CreateTransform(_position, dungeonRotation); var roomPackProtos = new Dictionary>(); - var externalNodes = new Dictionary>(); var fallbackTile = new Tile(_tileDefManager[prefab.Tile].TileId); foreach (var pack in _prototype.EnumeratePrototypes()) @@ -28,21 +27,6 @@ public sealed partial class DungeonJob var size = pack.Size; var sizePacks = roomPackProtos.GetOrNew(size); 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(); - 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). @@ -52,7 +36,7 @@ public sealed partial class DungeonJob string.Compare(x.ID, y.ID, StringComparison.Ordinal)); } - var roomProtos = new Dictionary>(); + var roomProtos = new Dictionary>(_prototype.Count()); foreach (var proto in _prototype.EnumeratePrototypes()) { @@ -80,60 +64,13 @@ public sealed partial class DungeonJob roomA.Sort((x, y) => 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[gen.RoomPacks.Count]; - - for (var i = 0; i < gen.RoomPacks.Count; i++) - { - var pack = gen.RoomPacks[i]; - var nodes = new HashSet(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>>(); - - 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(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 dungeon = new Dungeon() - { - Position = _position - }; + var dungeon = new Dungeon(); var availablePacks = new List(); var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count]; var packTransforms = new Matrix3[gen.RoomPacks.Count]; var packRotations = new Angle[gen.RoomPacks.Count]; - var rotatedPackNodes = new HashSet[gen.RoomPacks.Count]; // Actually pick the room packs and rooms for (var i = 0; i < gen.RoomPacks.Count; i++) @@ -159,9 +96,6 @@ public sealed partial class DungeonJob } // Iterate every pack - // To be valid it needs its edge nodes to overlap with every edge group - var external = connections[i]; - random.Shuffle(availablePacks); Matrix3 packTransform = default!; var found = false; @@ -169,11 +103,12 @@ public sealed partial class DungeonJob foreach (var aPack in availablePacks) { - var aExternal = externalNodes[aPack]; + var startIndex = random.Next(4); 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; if ((dir & (DirectionFlag.East | DirectionFlag.West)) != 0x0) @@ -190,37 +125,11 @@ public sealed partial class DungeonJob continue; found = true; - var rotatedNodes = new HashSet(aExternal.Count); 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 packTransform = Matrix3.CreateTransform(bounds.Center, aRotation); packRotations[i] = aRotation; - rotatedPackNodes[i] = rotatedNodes; pack = aPack; break; } @@ -311,6 +220,9 @@ public sealed partial class DungeonJob var templateGrid = _entManager.GetComponent(templateMapUid); var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize; var roomTiles = new HashSet(room.Size.X * room.Size.Y); + var exterior = new HashSet(room.Size.X * 2 + room.Size.Y * 2); + var tileOffset = -roomCenter + grid.TileSize / 2f; + Box2i? mapBounds = null; // Load tiles 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 tileRef = templateGrid.GetTileRef(indices); - var tilePos = dungeonMatty.Transform((Vector2) indices + grid.TileSize / 2f - roomCenter); + var tilePos = dungeonMatty.Transform(indices + tileOffset); var rounded = tilePos.Floored(); tiles.Add((rounded, tileRef.Tile)); 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; foreach (var tile in roomTiles) { - center += ((Vector2) tile + grid.TileSize / 2f); + center += (Vector2) tile + grid.TileSize / 2f; } center /= roomTiles.Count; - dungeon.Rooms.Add(new DungeonRoom(roomTiles, center)); + dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior)); grid.SetTiles(tiles); tiles.Clear(); var xformQuery = _entManager.GetEntityQuery(); @@ -344,7 +275,6 @@ public sealed partial class DungeonJob // Load entities // 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)) { @@ -391,7 +321,7 @@ public sealed partial class DungeonJob { 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); } @@ -399,6 +329,17 @@ public sealed partial class DungeonJob { 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(); @@ -427,16 +368,70 @@ public sealed partial class DungeonJob } } - // Calculate center + // Calculate center and do entrances var dungeonCenter = Vector2.Zero; 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; } + + 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; + } + } + } } diff --git a/Content.Server/Procedural/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob.PostGen.cs index a5d4d0101f..1aaaa4c31d 100644 --- a/Content.Server/Procedural/DungeonJob.PostGen.cs +++ b/Content.Server/Procedural/DungeonJob.PostGen.cs @@ -1,14 +1,16 @@ using System.Linq; using System.Threading.Tasks; -using Content.Server.Light.Components; +using Content.Server.NodeContainer; +using Content.Shared.Doors.Components; using Content.Shared.Physics; using Content.Shared.Procedural; using Content.Shared.Procedural.PostGeneration; using Content.Shared.Storage; +using Content.Shared.Tag; +using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Utility; @@ -23,33 +25,189 @@ public sealed partial class DungeonJob private const int CollisionMask = (int) CollisionGroup.Impassable; private const int CollisionLayer = (int) CollisionGroup.Impassable; + private bool HasWall(MapGridComponent grid, Vector2i tile) + { + var anchored = grid.GetAnchoredEntitiesEnumerator(tile); + + while (anchored.MoveNext(out var uid)) + { + if (_tagQuery.TryGetComponent(uid, out var tagComp) && tagComp.Tags.Contains("Wall")) + return true; + } + + return false; + } + + private async Task PostGen(AutoCablingPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, + Random random) + { + // There's a lot of ways you could do this. + // For now we'll just connect every LV cable in the dungeon. + var cableTiles = new HashSet(); + var allTiles = new HashSet(dungeon.CorridorTiles); + allTiles.UnionWith(dungeon.RoomTiles); + allTiles.UnionWith(dungeon.RoomExteriorTiles); + allTiles.UnionWith(dungeon.CorridorExteriorTiles); + var nodeQuery = _entManager.GetEntityQuery(); + + // Gather existing nodes + foreach (var tile in allTiles) + { + var anchored = grid.GetAnchoredEntitiesEnumerator(tile); + + while (anchored.MoveNext(out var anc)) + { + if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) || + !nodeContainer.Nodes.ContainsKey("power")) + { + continue; + } + + cableTiles.Add(tile); + break; + } + } + + // Iterating them all might be expensive. + await SuspendIfOutOfTime(); + + if (!ValidateResume()) + return; + + var startNodes = new List(cableTiles); + random.Shuffle(startNodes); + var start = startNodes[0]; + var remaining = new HashSet(startNodes); + var frontier = new PriorityQueue(); + frontier.Enqueue(start, 0f); + var cameFrom = new Dictionary(); + var costSoFar = new Dictionary(); + var lastDirection = new Dictionary(); + costSoFar[start] = 0f; + lastDirection[start] = Direction.Invalid; + var tagQuery = _entManager.GetEntityQuery(); + + // TODO: + // Pick a random node to start + // Then, dijkstra out from it. Add like +10 if it's a wall or smth + // When we hit another cable then mark it as found and iterate cameFrom and add to the thingie. + while (remaining.Count > 0) + { + if (frontier.Count == 0) + { + frontier.Enqueue(remaining.First(), 0f); + } + + var node = frontier.Dequeue(); + + if (remaining.Remove(node)) + { + var weh = node; + + while (cameFrom.TryGetValue(weh, out var receiver)) + { + cableTiles.Add(weh); + weh = receiver; + + if (weh == start) + break; + } + } + + if (!grid.TryGetTileRef(node, out var tileRef) || tileRef.Tile.IsEmpty) + { + continue; + } + + for (var i = 0; i < 4; i++) + { + var dir = (Direction) (i * 2); + + var neighbor = node + dir.ToIntVec(); + var tileCost = 1f; + + // Prefer straight lines. + if (lastDirection[node] != dir) + { + tileCost *= 1.1f; + } + + if (cableTiles.Contains(neighbor)) + { + tileCost *= 0.1f; + } + + // Prefer tiles without walls on them + if (HasWall(grid, neighbor)) + { + tileCost *= 20f; + } + + var gScore = costSoFar[node] + tileCost; + + if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue) + { + continue; + } + + cameFrom[neighbor] = node; + costSoFar[neighbor] = gScore; + lastDirection[neighbor] = dir; + frontier.Enqueue(neighbor, gScore); + } + } + + foreach (var tile in cableTiles) + { + var anchored = grid.GetAnchoredEntitiesEnumerator(tile); + var found = false; + + while (anchored.MoveNext(out var anc)) + { + if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) || + !nodeContainer.Nodes.ContainsKey("power")) + { + continue; + } + + found = true; + break; + } + + if (found) + continue; + + _entManager.SpawnEntity("CableApcExtension", _grid.GridTileToLocal(tile)); + } + } + private async Task PostGen(BoundaryWallPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { - var tile = new Tile(_tileDefManager[gen.Tile].TileId); - var tiles = new List<(Vector2i Index, Tile Tile)>(); + var tileDef = _tileDefManager[gen.Tile]; + var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count); // Spawn wall outline // - Tiles first - foreach (var room in dungeon.Rooms) + foreach (var neighbor in dungeon.RoomExteriorTiles) { - foreach (var index in room.Tiles) - { - for (var x = -1; x <= 1; x++) - { - for (var y = -1; y <= 1; y++) - { - var neighbor = new Vector2i(x + index.X, y + index.Y); + if (dungeon.RoomTiles.Contains(neighbor)) + continue; - if (dungeon.RoomTiles.Contains(neighbor)) - continue; + if (!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) + continue; - if (!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) - continue; + tiles.Add((neighbor, _tileDefManager.GetVariantTile(tileDef, random))); + } - tiles.Add((neighbor, tile)); - } - } - } + foreach (var index in dungeon.CorridorExteriorTiles) + { + if (dungeon.RoomTiles.Contains(index)) + continue; + + if (!_anchorable.TileFree(grid, index, CollisionLayer, CollisionMask)) + continue; + + tiles.Add((index, _tileDefManager.GetVariantTile(tileDef, random))); } grid.SetTiles(tiles); @@ -79,7 +237,7 @@ public sealed partial class DungeonJob var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y); - if (dungeon.RoomTiles.Contains(neighbor)) + if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor)) { isCorner = false; break; @@ -97,19 +255,181 @@ public sealed partial class DungeonJob if (!isCorner) _entManager.SpawnEntity(gen.Wall, grid.GridTileToLocal(index.Index)); - if (i % 10 == 0) + if (i % 20 == 0) { await SuspendIfOutOfTime(); - ValidateResume(); + + if (!ValidateResume()) + return; } } } - private async Task PostGen(EntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) + private async Task PostGen(CornerClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, + Random random) + { + var physicsQuery = _entManager.GetEntityQuery(); + var tagQuery = _entManager.GetEntityQuery(); + + foreach (var tile in dungeon.CorridorTiles) + { + var enumerator = _grid.GetAnchoredEntitiesEnumerator(tile); + var blocked = false; + + while (enumerator.MoveNext(out var ent)) + { + // TODO: TileFree + if (!physicsQuery.TryGetComponent(ent, out var physics) || + !physics.CanCollide || + !physics.Hard) + { + continue; + } + + blocked = true; + break; + } + + if (blocked) + continue; + + // If at least 2 adjacent tiles are blocked consider it a corner + for (var i = 0; i < 4; i++) + { + var dir = (Direction) (i * 2); + blocked = HasWall(grid, tile + dir.ToIntVec()); + + if (!blocked) + continue; + + var nextDir = (Direction) ((i + 1) * 2 % 8); + blocked = HasWall(grid, tile + nextDir.ToIntVec()); + + if (!blocked) + continue; + + if (random.Prob(gen.Chance)) + { + var coords = _grid.GridTileToLocal(tile); + var protos = EntitySpawnCollection.GetSpawns(gen.Contents, random); + _entManager.SpawnEntities(coords, protos); + } + + break; + } + } + } + + private async Task PostGen(CorridorDecalSkirtingPostGen decks, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) + { + var directions = new ValueList(4); + var pocketDirections = new ValueList(4); + var doorQuery = _entManager.GetEntityQuery(); + var physicsQuery = _entManager.GetEntityQuery(); + var offset = new Vector2(-_grid.TileSize / 2f, -_grid.TileSize / 2f); + var color = decks.Color; + + foreach (var tile in dungeon.CorridorTiles) + { + DebugTools.Assert(!dungeon.RoomTiles.Contains(tile)); + directions.Clear(); + + // Do cardinals 1 step + // Do corners the other step + for (var i = 0; i < 4; i++) + { + var dir = (DirectionFlag) Math.Pow(2, i); + var neighbor = tile + dir.AsDir().ToIntVec(); + + var anc = _grid.GetAnchoredEntitiesEnumerator(neighbor); + + while (anc.MoveNext(out var ent)) + { + if (!physicsQuery.TryGetComponent(ent, out var physics) || + !physics.CanCollide || + !physics.Hard || + doorQuery.HasComponent(ent.Value)) + { + continue; + } + + directions.Add(dir); + break; + } + } + + // Pockets + if (directions.Count == 0) + { + pocketDirections.Clear(); + + for (var i = 1; i < 5; i++) + { + var dir = (Direction) (i * 2 - 1); + var neighbor = tile + dir.ToIntVec(); + + var anc = _grid.GetAnchoredEntitiesEnumerator(neighbor); + + while (anc.MoveNext(out var ent)) + { + if (!physicsQuery.TryGetComponent(ent, out var physics) || + !physics.CanCollide || + !physics.Hard || + doorQuery.HasComponent(ent.Value)) + { + continue; + } + + pocketDirections.Add(dir); + break; + } + } + + if (pocketDirections.Count == 1) + { + if (decks.PocketDecals.TryGetValue(pocketDirections[0], out var cDir)) + { + // Decals not being centered biting my ass again + var gridPos = _grid.GridTileToLocal(tile).Offset(offset); + _decals.TryAddDecal(cDir, gridPos, out _, color: color); + } + } + + continue; + } + + if (directions.Count == 1) + { + if (decks.CardinalDecals.TryGetValue(directions[0], out var cDir)) + { + // Decals not being centered biting my ass again + var gridPos = _grid.GridTileToLocal(tile).Offset(offset); + _decals.TryAddDecal(cDir, gridPos, out _, color: color); + } + + continue; + } + + // Corners + if (directions.Count == 2) + { + // Auehghegueugegegeheh help me + var dirFlag = directions[0] | directions[1]; + + if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir)) + { + var gridPos = _grid.GridTileToLocal(tile).Offset(offset); + _decals.TryAddDecal(cDir, gridPos, out _, color: color); + } + } + } + } + + private async Task PostGen(DungeonEntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { var rooms = new List(dungeon.Rooms); var roomTiles = new List(); - var tileData = new Tile(_tileDefManager[gen.Tile].TileId); + var tileDef = _tileDefManager[gen.Tile]; for (var i = 0; i < gen.Count; i++) { @@ -119,52 +439,76 @@ public sealed partial class DungeonJob // Move out 3 tiles in a direction away from center of the room // If none of those intersect another tile it's probably external // TODO: Maybe need to take top half of furthest rooms in case there's interior exits? - roomTiles.AddRange(room.Tiles); + roomTiles.AddRange(room.Exterior); random.Shuffle(roomTiles); foreach (var tile in roomTiles) { - var direction = (tile - room.Center).ToAngle().GetCardinalDir().ToAngle().ToVec(); - var isValid = true; + var isValid = false; - for (var j = 1; j < 4; j++) + // Check if one side is dungeon and the other side is nothing. + for (var j = 0; j < 4; j++) { - var neighbor = (tile + direction * j).Floored(); + var dir = (Direction) (j * 2); + var oppositeDir = dir.GetOpposite(); + var dirVec = tile + dir.ToIntVec(); + var oppositeDirVec = tile + oppositeDir.ToIntVec(); - // If it's an interior tile or blocked. - if (dungeon.RoomTiles.Contains(neighbor) || _lookup.GetEntitiesIntersecting(gridUid, neighbor, LookupFlags.Dynamic | LookupFlags.Static).Any()) + if (!dungeon.RoomTiles.Contains(dirVec)) { - isValid = false; - break; - } - } - - if (!isValid) - continue; - - var entrancePos = (tile + direction).Floored(); - - // Entrance wew - grid.SetTile(entrancePos, tileData); - ClearDoor(dungeon, grid, entrancePos); - var gridCoords = grid.GridTileToLocal(entrancePos); - // Need to offset the spawn to avoid spawning in the room. - - foreach (var ent in gen.Entities) - { - _entManager.SpawnEntity(ent, gridCoords); - } - - // Clear out any biome tiles nearby to avoid blocking it - foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false)) - { - if (dungeon.RoomTiles.Contains(nearTile.GridIndices)) continue; + } - grid.SetTile(nearTile.GridIndices, tileData); + if (dungeon.RoomTiles.Contains(oppositeDirVec) || + dungeon.RoomExteriorTiles.Contains(oppositeDirVec) || + dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) || + dungeon.CorridorTiles.Contains(oppositeDirVec)) + { + continue; + } + + // Check if exterior spot free. + if (!_anchorable.TileFree(_grid, tile, CollisionLayer, CollisionMask)) + { + continue; + } + + // Check if interior spot free (no guarantees on exterior but ClearDoor should handle it) + if (!_anchorable.TileFree(_grid, dirVec, CollisionLayer, CollisionMask)) + { + continue; + } + + // Valid pick! + isValid = true; + + // Entrance wew + grid.SetTile(tile, _tileDefManager.GetVariantTile(tileDef, random)); + ClearDoor(dungeon, grid, tile); + var gridCoords = grid.GridTileToLocal(tile); + // Need to offset the spawn to avoid spawning in the room. + + _entManager.SpawnEntities(gridCoords, gen.Entities); + + // Clear out any biome tiles nearby to avoid blocking it + foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false)) + { + if (dungeon.RoomTiles.Contains(nearTile.GridIndices) || + dungeon.RoomExteriorTiles.Contains(nearTile.GridIndices) || + dungeon.CorridorTiles.Contains(nearTile.GridIndices) || + dungeon.CorridorExteriorTiles.Contains(nearTile.GridIndices)) + { + continue; + } + + grid.SetTile(nearTile.GridIndices, _tileDefManager.GetVariantTile(tileDef, random)); + } + + break; } - break; + if (isValid) + break; } roomTiles.Clear(); @@ -174,77 +518,106 @@ public sealed partial class DungeonJob private async Task PostGen(ExternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { - // Iterate every room with N chance to spawn windows on that wall per cardinal dir. - var chance = 0.25; - var distance = 10; + // Iterate every tile with N chance to spawn windows on that wall per cardinal dir. + var chance = 0.25 / 3f; - foreach (var room in dungeon.Rooms) + var allExterior = new HashSet(dungeon.CorridorExteriorTiles); + allExterior.UnionWith(dungeon.RoomExteriorTiles); + var validTiles = allExterior.ToList(); + random.Shuffle(validTiles); + + var tiles = new List<(Vector2i, Tile)>(); + var tileDef = _tileDefManager[gen.Tile]; + var count = Math.Floor(validTiles.Count * chance); + var index = 0; + var takenTiles = new HashSet(); + + // There's a bunch of shit here but tl;dr + // - don't spawn over cap + // - Check if we have 3 tiles in a row that aren't corners and aren't obstructed + foreach (var tile in validTiles) { - var validTiles = new List(); + if (index > count) + break; - for (var i = 0; i < 4; i++) + // Room tile / already used. + if (!_anchorable.TileFree(_grid, tile, CollisionLayer, CollisionMask) || + takenTiles.Contains(tile)) { - var dir = (DirectionFlag) Math.Pow(2, i); - var dirVec = dir.AsDir().ToIntVec(); + continue; + } - foreach (var tile in room.Tiles) + // Check we're not on a corner + for (var i = 0; i < 2; i++) + { + var dir = (Direction) (i * 2); + var dirVec = dir.ToIntVec(); + var isValid = true; + + // Check 1 beyond either side to ensure it's not a corner. + for (var j = -1; j < 4; j++) { - var tileAngle = ((Vector2) tile + grid.TileSize / 2f - room.Center).ToAngle(); - var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2); + var neighbor = tile + dirVec * j; - var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded(); - - if (!tileVec.Equals(dirVec)) - continue; - - var valid = true; - - for (var j = 1; j < distance; j++) + if (!allExterior.Contains(neighbor) || + takenTiles.Contains(neighbor) || + !_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) { - var edgeNeighbor = tile + dirVec * j; + isValid = false; + break; + } - if (dungeon.RoomTiles.Contains(edgeNeighbor)) + // Also check perpendicular that it is free + foreach (var k in new [] {2, 6}) + { + var perp = (Direction) ((i * 2 + k) % 8); + var perpVec = perp.ToIntVec(); + var perpTile = tile + perpVec; + + if (allExterior.Contains(perpTile) || + takenTiles.Contains(neighbor) || + !_anchorable.TileFree(_grid, perpTile, CollisionLayer, CollisionMask)) { - valid = false; + isValid = false; break; } } - if (!valid) - continue; - - var windowTile = tile + dirVec; - - if (!_anchorable.TileFree(grid, windowTile, CollisionLayer, CollisionMask)) - continue; - - validTiles.Add(windowTile); + if (!isValid) + break; } - if (validTiles.Count == 0 || random.NextDouble() > chance) + if (!isValid) continue; - validTiles.Sort((x, y) => ((Vector2) x + grid.TileSize / 2f - room.Center).LengthSquared.CompareTo(((Vector2) y + grid.TileSize / 2f - room.Center).LengthSquared)); - - for (var j = 0; j < Math.Min(validTiles.Count, 3); j++) + for (var j = 0; j < 3; j++) { - var tile = validTiles[j]; - var gridPos = grid.GridTileToLocal(tile); - grid.SetTile(tile, new Tile(_tileDefManager[gen.Tile].TileId)); + var neighbor = tile + dirVec * j; - foreach (var ent in gen.Entities) - { - _entManager.SpawnEntity(ent, gridPos); - } + tiles.Add((neighbor, _tileDefManager.GetVariantTile(tileDef, random))); + index++; + takenTiles.Add(neighbor); } + } + } - if (validTiles.Count > 0) - { - await SuspendIfOutOfTime(); - ValidateResume(); - } + grid.SetTiles(tiles); + index = 0; - validTiles.Clear(); + foreach (var tile in tiles) + { + var gridPos = grid.GridTileToLocal(tile.Item1); + + index += gen.Entities.Count; + _entManager.SpawnEntities(gridPos, gen.Entities); + + if (index > 20) + { + index -= 20; + await SuspendIfOutOfTime(); + + if (!ValidateResume()) + return; } } } @@ -263,6 +636,7 @@ public sealed partial class DungeonJob // If so then consider windows var minDistance = 4; var maxDistance = 6; + var tileDef = _tileDefManager[gen.Tile]; foreach (var room in dungeon.Rooms) { @@ -321,18 +695,17 @@ public sealed partial class DungeonJob { var tile = validTiles[j]; var gridPos = grid.GridTileToLocal(tile); - grid.SetTile(tile, new Tile(_tileDefManager[gen.Tile].TileId)); + grid.SetTile(tile, _tileDefManager.GetVariantTile(tileDef, random)); - foreach (var ent in gen.Entities) - { - _entManager.SpawnEntity(ent, gridPos); - } + _entManager.SpawnEntities(gridPos, gen.Entities); } if (validTiles.Count > 0) { await SuspendIfOutOfTime(); - ValidateResume(); + + if (!ValidateResume()) + return; } validTiles.Clear(); @@ -340,6 +713,333 @@ public sealed partial class DungeonJob } } + /// + /// Simply places tiles / entities on the entrances to rooms. + /// + private async Task PostGen(RoomEntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, + Random random) + { + var setTiles = new List<(Vector2i, Tile)>(); + var tileDef = _tileDefManager[gen.Tile]; + + foreach (var room in dungeon.Rooms) + { + foreach (var entrance in room.Entrances) + { + setTiles.Add((entrance, _tileDefManager.GetVariantTile(tileDef, random))); + } + } + + grid.SetTiles(setTiles); + + foreach (var room in dungeon.Rooms) + { + foreach (var entrance in room.Entrances) + { + _entManager.SpawnEntities(grid.GridTileToLocal(entrance), gen.Entities); + } + } + } + + /// + /// Generates corridor connections between entrances to all the rooms. + /// + private async Task PostGen(CorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) + { + var entrances = new List(dungeon.Rooms.Count); + + // Grab entrances + foreach (var room in dungeon.Rooms) + { + entrances.AddRange(room.Entrances); + } + + var edges = _dungeon.MinimumSpanningTree(entrances, random); + await SuspendIfOutOfTime(); + + if (!ValidateResume()) + return; + + // TODO: Add in say 1/3 of edges back in to add some cyclic to it. + + var expansion = gen.Width - 2; + // Okay so tl;dr is that we don't want to cut close to rooms as it might go from 3 width to 2 width suddenly + // So we will add a buffer range around each room to deter pathfinding there unless necessary + var deterredTiles = new HashSet(); + + if (expansion >= 1) + { + foreach (var tile in dungeon.RoomExteriorTiles) + { + for (var x = -expansion; x <= expansion; x++) + { + for (var y = -expansion; y <= expansion; y++) + { + var neighbor = new Vector2i(tile.X + x, tile.Y + y); + + if (dungeon.RoomTiles.Contains(neighbor) || + dungeon.RoomExteriorTiles.Contains(neighbor) || + entrances.Contains(neighbor)) + { + continue; + } + + deterredTiles.Add(neighbor); + } + } + } + } + + foreach (var room in dungeon.Rooms) + { + foreach (var entrance in room.Entrances) + { + // Just so we can still actually get in to the entrance we won't deter from a tile away from it. + var normal = ((Vector2) entrance + grid.TileSize / 2f - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec(); + deterredTiles.Remove(entrance + normal); + } + } + + var excludedTiles = new HashSet(dungeon.RoomExteriorTiles); + excludedTiles.UnionWith(dungeon.RoomTiles); + var corridorTiles = new HashSet(); + + _dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile => + { + var mod = 1f; + + if (corridorTiles.Contains(tile)) + { + mod *= 0.1f; + } + + if (deterredTiles.Contains(tile)) + { + mod *= 2f; + } + + return mod; + }); + + // Widen the path + if (expansion >= 1) + { + var toAdd = new ValueList(); + + foreach (var node in corridorTiles) + { + // Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug + // exterior walls and make the path smaller. + + for (var x = -expansion; x <= expansion; x++) + { + for (var y = -expansion; y <= expansion; y++) + { + var neighbor = new Vector2i(node.X + x, node.Y + y); + + // Diagonals still matter here. + if (dungeon.RoomTiles.Contains(neighbor) || + dungeon.RoomExteriorTiles.Contains(neighbor)) + { + // Try + + continue; + } + + toAdd.Add(neighbor); + } + } + } + + foreach (var node in toAdd) + { + corridorTiles.Add(node); + } + } + + var setTiles = new List<(Vector2i, Tile)>(); + var tileDef = _tileDefManager["FloorSteel"]; + + foreach (var tile in corridorTiles) + { + setTiles.Add((tile, _tileDefManager.GetVariantTile(tileDef, random))); + } + + grid.SetTiles(setTiles); + dungeon.CorridorTiles.UnionWith(corridorTiles); + + var exterior = dungeon.CorridorExteriorTiles; + + // Just ignore entrances or whatever for now. + foreach (var tile in dungeon.CorridorTiles) + { + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + var neighbor = new Vector2i(tile.X + x, tile.Y + y); + + if (dungeon.CorridorTiles.Contains(neighbor)) + continue; + + exterior.Add(neighbor); + } + } + } + } + + private async Task PostGen(EntranceFlankPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, + Random random) + { + var tiles = new List<(Vector2i Index, Tile)>(); + var tileDef = _tileDefManager[gen.Tile]; + var spawnPositions = new ValueList(dungeon.Rooms.Count); + + foreach (var room in dungeon.Rooms) + { + foreach (var entrance in room.Entrances) + { + for (var i = 0; i < 8; i++) + { + var dir = (Direction) i; + var neighbor = entrance + dir.ToIntVec(); + + if (!dungeon.RoomExteriorTiles.Contains(neighbor)) + continue; + + tiles.Add((neighbor, _tileDefManager.GetVariantTile(tileDef, random))); + spawnPositions.Add(neighbor); + } + } + } + + grid.SetTiles(tiles); + + foreach (var entrance in spawnPositions) + { + _entManager.SpawnEntities(_grid.GridTileToLocal(entrance), gen.Entities); + } + } + + private async Task PostGen(JunctionPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, + Random random) + { + var tileDef = _tileDefManager[gen.Tile]; + + // N-wide junctions + foreach (var tile in dungeon.CorridorTiles) + { + if (!_anchorable.TileFree(_grid, tile, CollisionLayer, CollisionMask)) + continue; + + // Check each direction: + // - Check if immediate neighbors are free + // - Check if the neighbors beyond that are not free + // - Then check either side if they're slightly more free + var exteriorWidth = (int) Math.Floor(gen.Width / 2f); + var width = (int) Math.Ceiling(gen.Width / 2f); + + for (var i = 0; i < 2; i++) + { + var isValid = true; + var neighborDir = (Direction) (i * 2); + var neighborVec = neighborDir.ToIntVec(); + + for (var j = -width; j <= width; j++) + { + if (j == 0) + continue; + + var neighbor = tile + neighborVec * j; + + // If it's an end tile then check it's occupied. + if (j == -width || + j == width) + { + if (!HasWall(grid, neighbor)) + { + isValid = false; + break; + } + + continue; + } + + // If we're not at the end tile then check it + perpendicular are free. + if (!_anchorable.TileFree(_grid, neighbor, CollisionLayer, CollisionMask)) + { + isValid = false; + break; + } + + var perp1 = tile + neighborVec * j + ((Direction) ((i * 2 + 2) % 8)).ToIntVec(); + var perp2 = tile + neighborVec * j + ((Direction) ((i * 2 + 6) % 8)).ToIntVec(); + + if (!_anchorable.TileFree(_grid, perp1, CollisionLayer, CollisionMask)) + { + isValid = false; + break; + } + + if (!_anchorable.TileFree(_grid, perp2, CollisionLayer, CollisionMask)) + { + isValid = false; + break; + } + } + + if (!isValid) + continue; + + // Check corners to see if either side opens up (if it's just a 1x wide corridor do nothing, needs to be a funnel. + foreach (var j in new [] {-exteriorWidth, exteriorWidth}) + { + var freeCount = 0; + + // Need at least 3 of 4 free + for (var k = 0; k < 4; k++) + { + var cornerDir = (Direction) (k * 2 + 1); + var cornerVec = cornerDir.ToIntVec(); + var cornerNeighbor = tile + neighborVec * j + cornerVec; + + if (_anchorable.TileFree(_grid, cornerNeighbor, CollisionLayer, CollisionMask)) + { + freeCount++; + } + } + + if (freeCount < gen.Width) + continue; + + // Valid! + isValid = true; + + for (var x = -width + 1; x < width; x++) + { + var weh = tile + neighborDir.ToIntVec() * x; + grid.SetTile(weh, _tileDefManager.GetVariantTile(tileDef, random)); + + var coords = grid.GridTileToLocal(weh); + _entManager.SpawnEntities(coords, gen.Entities); + } + + break; + } + + if (isValid) + { + await SuspendIfOutOfTime(); + + if (!ValidateResume()) + return; + } + + break; + } + } + } + private async Task PostGen(MiddleConnectionPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { // TODO: Need a minimal spanning tree version tbh @@ -387,7 +1087,7 @@ public sealed partial class DungeonJob var roomConnections = new Dictionary>(); var frontier = new Queue(); frontier.Enqueue(dungeon.Rooms.First()); - var tile = new Tile(_tileDefManager[gen.Tile].TileId); + var tileDef = _tileDefManager[gen.Tile]; foreach (var (room, border) in roomBorders) { @@ -436,24 +1136,18 @@ public sealed partial class DungeonJob continue; width--; - grid.SetTile(node, tile); + grid.SetTile(node, _tileDefManager.GetVariantTile(tileDef, random)); if (gen.EdgeEntities != null && nodeDistances.Count - i <= 2) { - foreach (var ent in gen.EdgeEntities) - { - _entManager.SpawnEntity(ent, gridPos); - } + _entManager.SpawnEntities(gridPos, gen.EdgeEntities); } else { // Iterate neighbors and check for blockers, if so bulldoze ClearDoor(dungeon, grid, node); - foreach (var ent in gen.Entities) - { - _entManager.SpawnEntity(ent, gridPos); - } + _entManager.SpawnEntities(gridPos, gen.Entities); } if (width == 0) @@ -464,7 +1158,9 @@ public sealed partial class DungeonJob var otherConns = roomConnections.GetOrNew(otherRoom); otherConns.Add(room); await SuspendIfOutOfTime(); - ValidateResume(); + + if (!ValidateResume()) + return; } } } @@ -511,40 +1207,35 @@ public sealed partial class DungeonJob private async Task PostGen(WallMountPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { - var tileDef = new Tile(_tileDefManager[gen.Tile].TileId); + var tileDef = _tileDefManager[gen.Tile]; var checkedTiles = new HashSet(); + var allExterior = new HashSet(dungeon.CorridorExteriorTiles); + allExterior.UnionWith(dungeon.RoomExteriorTiles); + var count = 0; - foreach (var room in dungeon.Rooms) + foreach (var neighbor in allExterior) { - foreach (var tile in room.Tiles) + // Occupado + if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) + continue; + + if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor)) + continue; + + grid.SetTile(neighbor, _tileDefManager.GetVariantTile(tileDef, random)); + var gridPos = grid.GridTileToLocal(neighbor); + var protoNames = EntitySpawnCollection.GetSpawns(gen.Spawns, random); + + _entManager.SpawnEntities(gridPos, protoNames); + count += protoNames.Count; + + if (count > 20) { - for (var x = -1; x <= 1; x++) - { - for (var y = -1; y <= 1; y++) - { - if (x != 0 && y != 0) - { - continue; - } + count -= 20; + await SuspendIfOutOfTime(); - var neighbor = new Vector2i(tile.X + x, tile.Y + y); - - // Occupado - if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) - continue; - - if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor)) - continue; - - grid.SetTile(neighbor, tileDef); - var gridPos = grid.GridTileToLocal(neighbor); - - foreach (var ent in EntitySpawnCollection.GetSpawns(gen.Spawns, random)) - { - _entManager.SpawnEntity(ent, gridPos); - } - } - } + if (!ValidateResume()) + return; } } } diff --git a/Content.Server/Procedural/DungeonJob.cs b/Content.Server/Procedural/DungeonJob.cs index 7d61ad10d8..f2093e51cf 100644 --- a/Content.Server/Procedural/DungeonJob.cs +++ b/Content.Server/Procedural/DungeonJob.cs @@ -6,10 +6,12 @@ using Content.Server.Decals; using Content.Shared.Procedural; using Content.Shared.Procedural.DungeonGenerators; using Content.Shared.Procedural.PostGeneration; +using Content.Shared.Tag; using Robust.Server.Physics; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Server.Procedural; @@ -25,6 +27,7 @@ public sealed partial class DungeonJob : Job private readonly DungeonSystem _dungeon; private readonly EntityLookupSystem _lookup; private readonly SharedTransformSystem _transform; + private EntityQuery _tagQuery; private readonly DungeonConfigPrototype _gen; private readonly int _seed; @@ -65,6 +68,7 @@ public sealed partial class DungeonJob : Job _dungeon = dungeon; _lookup = lookup; _transform = transform; + _tagQuery = _entManager.GetEntityQuery(); _gen = gen; _grid = grid; @@ -88,10 +92,8 @@ public sealed partial class DungeonJob : Job throw new NotImplementedException(); } - foreach (var room in dungeon.Rooms) - { - dungeon.RoomTiles.UnionWith(room.Tiles); - } + DebugTools.Assert(dungeon.RoomTiles.Count > 0); + DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0); // To make it slightly more deterministic treat this RNG as separate ig. var random = new Random(_seed); @@ -102,10 +104,31 @@ public sealed partial class DungeonJob : Job 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: await PostGen(dordor, dungeon, _gridUid, _grid, random); break; - case EntrancePostGen entrance: + case DungeonEntrancePostGen entrance: await PostGen(entrance, dungeon, _gridUid, _grid, random); break; case ExternalWindowPostGen externalWindow: @@ -114,8 +137,8 @@ public sealed partial class DungeonJob : Job case InternalWindowPostGen internalWindow: await PostGen(internalWindow, dungeon, _gridUid, _grid, random); break; - case BoundaryWallPostGen boundary: - await PostGen(boundary, dungeon, _gridUid, _grid, random); + case RoomEntrancePostGen rEntrance: + await PostGen(rEntrance, dungeon, _gridUid, _grid, random); break; case WallMountPostGen wall: await PostGen(wall, dungeon, _gridUid, _grid, random); @@ -125,7 +148,9 @@ public sealed partial class DungeonJob : Job } await SuspendIfOutOfTime(); - ValidateResume(); + + if (!ValidateResume()) + break; } _grid.CanSplit = true; diff --git a/Content.Server/Procedural/DungeonSystem.Helpers.cs b/Content.Server/Procedural/DungeonSystem.Helpers.cs new file mode 100644 index 0000000000..35a21ff07a --- /dev/null +++ b/Content.Server/Procedural/DungeonSystem.Helpers.cs @@ -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 tiles, System.Random random) + { + // Generate connections between all rooms. + var connections = new Dictionary>(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(tiles); + remaining.RemoveAt(seedIndex); + + var edges = new List<(Vector2i Start, Vector2i End)>(); + + var seedEntrance = tiles[seedIndex]; + var forest = new ValueList(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; + } + + /// + /// Primarily for dungeon usage. + /// + public void GetCorridorNodes(HashSet corridorTiles, + List<(Vector2i Start, Vector2i End)> edges, + int pathLimit, + HashSet? forbiddenTiles = null, + Func? tileCallback = null) + { + // Pathfind each entrance + var frontier = new PriorityQueue(); + var cameFrom = new Dictionary(); + var directions = new Dictionary(); + var costSoFar = new Dictionary(); + forbiddenTiles ??= new HashSet(); + + 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); + } + } + } + } +} diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index 00a06ac754..f2be4fbbcc 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -15,7 +15,7 @@ using Robust.Shared.Prototypes; 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 IConsoleHost _console = default!; diff --git a/Content.Server/UserInterface/StatValuesCommand.cs b/Content.Server/UserInterface/StatValuesCommand.cs index 7356a0204a..b13b596323 100644 --- a/Content.Server/UserInterface/StatValuesCommand.cs +++ b/Content.Server/UserInterface/StatValuesCommand.cs @@ -24,7 +24,7 @@ public sealed class StatValuesCommand : IConsoleCommand public string Command => "showvalues"; public string Description => Loc.GetString("stat-values-desc"); - public string Help => $"{Command} "; + public string Help => $"{Command} "; public void Execute(IConsoleShell shell, string argStr, string[] args) { if (shell.Player is not IPlayerSession pSession) diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 1b4d551c13..24983ff37b 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -25,7 +25,7 @@ public sealed class StaminaComponent : Component /// How much time after receiving damage until stamina starts decreasing. /// [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")] - public float DecayCooldown = 5f; + public float DecayCooldown = 3f; /// /// How much stamina damage this entity has taken. diff --git a/Content.Shared/NPC/SharedPathfindingSystem.cs b/Content.Shared/NPC/SharedPathfindingSystem.cs index c46450b764..b12d2e68d4 100644 --- a/Content.Shared/NPC/SharedPathfindingSystem.cs +++ b/Content.Shared/NPC/SharedPathfindingSystem.cs @@ -19,4 +19,17 @@ public abstract class SharedPathfindingSystem : EntitySystem { 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); + } } diff --git a/Content.Shared/Procedural/Dungeon.cs b/Content.Shared/Procedural/Dungeon.cs index 6aef0c1bdf..1a465f8fca 100644 --- a/Content.Shared/Procedural/Dungeon.cs +++ b/Content.Shared/Procedural/Dungeon.cs @@ -2,17 +2,16 @@ namespace Content.Shared.Procedural; public sealed class Dungeon { - /// - /// Starting position used to generate the dungeon from. - /// - public Vector2i Position; - - public Vector2i Center; - - public List Rooms = new(); + public readonly List Rooms = new(); /// /// Hashset of the tiles across all rooms. /// - public HashSet RoomTiles = new(); + public readonly HashSet RoomTiles = new(); + + public readonly HashSet RoomExteriorTiles = new(); + + public readonly HashSet CorridorTiles = new(); + + public readonly HashSet CorridorExteriorTiles = new(); } diff --git a/Content.Shared/Procedural/DungeonRoom.cs b/Content.Shared/Procedural/DungeonRoom.cs index abd19922f2..86f404b3cf 100644 --- a/Content.Shared/Procedural/DungeonRoom.cs +++ b/Content.Shared/Procedural/DungeonRoom.cs @@ -1,3 +1,11 @@ namespace Content.Shared.Procedural; -public sealed record DungeonRoom(HashSet Tiles, Vector2 Center); +public sealed record DungeonRoom(HashSet Tiles, Vector2 Center, Box2i Bounds, HashSet Exterior) +{ + public List Entrances = new(); + + /// + /// Nodes adjacent to tiles, including the corners. + /// + public readonly HashSet Exterior = Exterior; +} diff --git a/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs b/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs new file mode 100644 index 0000000000..dccaa8ab18 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Runs cables throughout the dungeon. +/// +public sealed class AutoCablingPostGen : IPostDunGen +{ + +} diff --git a/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs b/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs new file mode 100644 index 0000000000..7cb28a1f27 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs @@ -0,0 +1,18 @@ +using Content.Shared.Storage; + +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Spawns entities inside corners. +/// +public sealed class CornerClutterPostGen : IPostDunGen +{ + [DataField("chance")] + public float Chance = 0.50f; + + /// + /// The default starting bulbs + /// + [DataField("contents", required: true)] + public List Contents = new(); +} diff --git a/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs b/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs new file mode 100644 index 0000000000..7615c85281 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs @@ -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; + +/// +/// Applies decal skirting to corridors. +/// +public sealed class CorridorDecalSkirtingPostGen : IPostDunGen +{ + /// + /// Color to apply to decals. + /// + [DataField("color")] + public Color? Color; + + /// + /// Decal where 1 edge is found. + /// + [DataField("cardinalDecals")] + public Dictionary CardinalDecals = new(); + + /// + /// Decal where 1 corner edge is found. + /// + [DataField("pocketDecals")] + public Dictionary PocketDecals = new(); + + /// + /// Decal where 2 or 3 edges are found. + /// + [DataField("cornerDecals")] + public Dictionary CornerDecals = new(); +} diff --git a/Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs b/Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs new file mode 100644 index 0000000000..27e8cdbad8 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs @@ -0,0 +1,31 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Connects room entrances via corridor segments. +/// +public sealed class CorridorPostGen : IPostDunGen +{ + /// + /// How far we're allowed to generate a corridor before calling it. + /// + /// + /// Given the heavy weightings this needs to be fairly large for larger dungeons. + /// + [DataField("pathLimit")] + public int PathLimit = 2048; + + [DataField("method")] + public CorridorPostGenMethod Method = CorridorPostGenMethod.MinimumSpanningTree; + + /// + /// How wide to make the corridor. + /// + [DataField("width")] + public int Width = 3; +} + +public enum CorridorPostGenMethod : byte +{ + Invalid, + MinimumSpanningTree, +} diff --git a/Content.Shared/Procedural/PostGeneration/EntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs similarity index 83% rename from Content.Shared/Procedural/PostGeneration/EntrancePostGen.cs rename to Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs index 8e5f5f5418..360179404d 100644 --- a/Content.Shared/Procedural/PostGeneration/EntrancePostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs @@ -8,7 +8,7 @@ namespace Content.Shared.Procedural.PostGeneration; /// /// Selects [count] rooms and places external doors to them. /// -public sealed class EntrancePostGen : IPostDunGen +public sealed class DungeonEntrancePostGen : IPostDunGen { /// /// How many rooms we place doors on. @@ -17,9 +17,10 @@ public sealed class EntrancePostGen : IPostDunGen public int Count = 1; [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() + public List Entities = new() { - "AirlockGlass" + "CableApcExtension", + "AirlockGlass", }; [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] diff --git a/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs b/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs new file mode 100644 index 0000000000..b16cae5e9d --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs @@ -0,0 +1,16 @@ +using Content.Shared.Maps; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Spawns entities on either side of an entrance. +/// +public sealed class EntranceFlankPostGen : IPostDunGen +{ + [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Tile = "FloorSteel"; + + [DataField("entities")] + public List Entities = new(); +} diff --git a/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs index 507ef8acfb..3ecdd55f5a 100644 --- a/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs @@ -11,7 +11,7 @@ namespace Content.Shared.Procedural.PostGeneration; public sealed class ExternalWindowPostGen : IPostDunGen { [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() + public List Entities = new() { "Grille", "Window", diff --git a/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs index d1f3818c57..5142d007b1 100644 --- a/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs @@ -11,7 +11,7 @@ namespace Content.Shared.Procedural.PostGeneration; public sealed class InternalWindowPostGen : IPostDunGen { [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() + public List Entities = new() { "Grille", "Window", diff --git a/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs b/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs new file mode 100644 index 0000000000..360dc4d3ab --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs @@ -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; + +/// +/// Places the specified entities at junction areas. +/// +public sealed class JunctionPostGen : IPostDunGen +{ + /// + /// Width to check for junctions. + /// + [DataField("width")] + public int Width = 3; + + [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Tile = "FloorSteel"; + + [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Entities = new() + { + "CableApcExtension", + "AirlockGlass" + }; +} diff --git a/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs b/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs index 8a16bb93f2..5cb0618a79 100644 --- a/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs @@ -26,7 +26,7 @@ public sealed class MiddleConnectionPostGen : IPostDunGen public string Tile = "FloorSteel"; [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() + public List Entities = new() { "CableApcExtension", "AirlockGlass" @@ -35,5 +35,5 @@ public sealed class MiddleConnectionPostGen : IPostDunGen /// /// If overlap > 1 then what should spawn on the edges. /// - [DataField("edgeEntities")] public List? EdgeEntities; + [DataField("edgeEntities")] public List EdgeEntities = new(); } diff --git a/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs new file mode 100644 index 0000000000..6db729cb8c --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs @@ -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; + +/// +/// Places tiles / entities onto room entrances. +/// +public sealed class RoomEntrancePostGen : IPostDunGen +{ + [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Entities = new() + { + "CableApcExtension", + "AirlockGlass", + }; + + [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Tile = "FloorSteel"; +} diff --git a/Content.Shared/Procedural/SharedDungeonSystem.cs b/Content.Shared/Procedural/SharedDungeonSystem.cs new file mode 100644 index 0000000000..f5d97f7fd4 --- /dev/null +++ b/Content.Shared/Procedural/SharedDungeonSystem.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Procedural; + +public abstract class SharedDungeonSystem : EntitySystem +{ + +} diff --git a/Resources/Prototypes/Procedural/dungeon_configs.yml b/Resources/Prototypes/Procedural/dungeon_configs.yml index ddb84815d2..732a5411f2 100644 --- a/Resources/Prototypes/Procedural/dungeon_configs.yml +++ b/Resources/Prototypes/Procedural/dungeon_configs.yml @@ -4,32 +4,23 @@ roomWhitelist: - SalvageExperiment presets: - - Cross - - SpaceMan - - FourSquare + - Bucket + - Wow + - SpaceShip + - Tall postGeneration: - - !type:MiddleConnectionPostGen - overlapCount: 3 - count: 3 - entities: - - CableApcExtension - - AirlockGlass - edgeEntities: - - Grille - - Window + - !type:CorridorPostGen + width: 3 - - !type:MiddleConnectionPostGen - count: 1 - entities: - - CableApcExtension - - AirlockGlass - - - !type:EntrancePostGen + - !type:DungeonEntrancePostGen count: 2 + + - !type:RoomEntrancePostGen entities: + - CableApcExtension - AirlockGlass - - !type:InternalWindowPostGen + - !type:EntranceFlankPostGen entities: - Grille - Window @@ -59,38 +50,60 @@ wall: WallSolid 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 id: LavaBrig generator: !type:PrefabDunGen roomWhitelist: - LavaBrig presets: - - Cross - - SpaceMan - - FourSquare + - Bucket + - Wow + - SpaceShip + - Tall postGeneration: - - !type:MiddleConnectionPostGen - overlapCount: 3 - count: 3 - entities: - - CableApcExtension - - AirlockSecurityGlassLocked - edgeEntities: - - Grille - - Window + - !type:CorridorPostGen + width: 3 - - !type:MiddleConnectionPostGen - count: 1 - entities: - - CableApcExtension - - AirlockSecurityGlassLocked - - - !type:EntrancePostGen + - !type:DungeonEntrancePostGen count: 2 + + - !type:RoomEntrancePostGen entities: + - CableApcExtension - AirlockSecurityGlassLocked - - !type:InternalWindowPostGen + - !type:EntranceFlankPostGen entities: - Grille - Window @@ -98,7 +111,7 @@ - !type:ExternalWindowPostGen entities: - Grille - - ReinforcedWindow + - Window - !type:WallMountPostGen spawns: @@ -117,5 +130,35 @@ - !type:BoundaryWallPostGen tile: FloorSteel - wall: WallReinforced + wall: WallSolid 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 diff --git a/Resources/Prototypes/Procedural/dungeon_presets.yml b/Resources/Prototypes/Procedural/dungeon_presets.yml index 80617418c8..f70ad32d25 100644 --- a/Resources/Prototypes/Procedural/dungeon_presets.yml +++ b/Resources/Prototypes/Procedural/dungeon_presets.yml @@ -1,37 +1,35 @@ -# Dungeon presets - type: dungeonPreset - id: Cross + id: Bucket roomPacks: - - -8,0,9,5 - - -2,6,3,11 - # Offset to the first one - - -8,12,9,17 - - -2,18,3,35 - - -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 + - 0,0,17,17 + - 20,0,37,17 + - 20,20,37,37 + - -20,0,-3,17 + - -20,20,-3,37 - type: dungeonPreset - id: FourSquare + id: SpaceShip roomPacks: - - -38,18,-21,35 - - -8,36,9,53 - - 22,18,39,35 - - -8,0,9,17 - - -2,18,3,35 - - -20,24,-3,29 - - 4,24,21,29 + - 0,10,17,27 + - 20,0,37,17 + - 20,20,37,37 + - -20,0,-3,17 + - -20,20,-3,37 + +- 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 diff --git a/Resources/Prototypes/Procedural/dungeon_room_packs.yml b/Resources/Prototypes/Procedural/dungeon_room_packs.yml index 28bf1481cd..a6da1dad98 100644 --- a/Resources/Prototypes/Procedural/dungeon_room_packs.yml +++ b/Resources/Prototypes/Procedural/dungeon_room_packs.yml @@ -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 id: LargeArea2 size: 17,17 rooms: - - 7,0,10,11 - - 6,12,11,17 - - 12,13,17,16 + - 1,2,8,7 + - 1,10,8,15 + - 13,2,16,15 -# Wide corridor vertically up the middle and small corridors on left - type: dungeonRoomPack id: LargeArea3 size: 17,17 rooms: - - 6,0,11,11 - - 6,12,11,17 - - 0,13,5,16 - - 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 \ No newline at end of file + - 2,3,5,8 + - 1,10,6,15 + - 13,2,16,15