1243 lines
42 KiB
C#
1243 lines
42 KiB
C#
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
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.Random;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Procedural;
|
|
|
|
public sealed partial class DungeonJob
|
|
{
|
|
/*
|
|
* Run after the main dungeon generation
|
|
*/
|
|
|
|
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<Vector2i>();
|
|
var allTiles = new HashSet<Vector2i>(dungeon.CorridorTiles);
|
|
allTiles.UnionWith(dungeon.RoomTiles);
|
|
allTiles.UnionWith(dungeon.RoomExteriorTiles);
|
|
allTiles.UnionWith(dungeon.CorridorExteriorTiles);
|
|
var nodeQuery = _entManager.GetEntityQuery<NodeContainerComponent>();
|
|
|
|
// Gather existing nodes
|
|
foreach (var tile in allTiles)
|
|
{
|
|
var anchored = 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<Vector2i>(cableTiles);
|
|
random.Shuffle(startNodes);
|
|
var start = startNodes[0];
|
|
var remaining = new HashSet<Vector2i>(startNodes);
|
|
var frontier = new PriorityQueue<Vector2i, float>();
|
|
frontier.Enqueue(start, 0f);
|
|
var cameFrom = new Dictionary<Vector2i, Vector2i>();
|
|
var costSoFar = new Dictionary<Vector2i, float>();
|
|
var lastDirection = new Dictionary<Vector2i, Direction>();
|
|
costSoFar[start] = 0f;
|
|
lastDirection[start] = Direction.Invalid;
|
|
var tagQuery = _entManager.GetEntityQuery<TagComponent>();
|
|
|
|
// 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 tileDef = _tileDefManager[gen.Tile];
|
|
var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
|
|
|
|
// Spawn wall outline
|
|
// - Tiles first
|
|
foreach (var neighbor in dungeon.RoomExteriorTiles)
|
|
{
|
|
if (dungeon.RoomTiles.Contains(neighbor))
|
|
continue;
|
|
|
|
if (!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask))
|
|
continue;
|
|
|
|
tiles.Add((neighbor, _tileDefManager.GetVariantTile(tileDef, random)));
|
|
}
|
|
|
|
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);
|
|
|
|
// Double iteration coz we bulk set tiles for speed.
|
|
for (var i = 0; i < tiles.Count; i++)
|
|
{
|
|
var index = tiles[i];
|
|
if (!_anchorable.TileFree(grid, index.Index, CollisionLayer, CollisionMask))
|
|
continue;
|
|
|
|
// If no cardinal neighbors in dungeon then we're a corner.
|
|
var isCorner = false;
|
|
|
|
if (gen.CornerWall != null)
|
|
{
|
|
isCorner = true;
|
|
|
|
for (var x = -1; x <= 1; x++)
|
|
{
|
|
for (var y = -1; y <= 1; y++)
|
|
{
|
|
if (x != 0 && y != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y);
|
|
|
|
if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor))
|
|
{
|
|
isCorner = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isCorner)
|
|
break;
|
|
}
|
|
|
|
if (isCorner)
|
|
_entManager.SpawnEntity(gen.CornerWall, grid.GridTileToLocal(index.Index));
|
|
}
|
|
|
|
if (!isCorner)
|
|
_entManager.SpawnEntity(gen.Wall, grid.GridTileToLocal(index.Index));
|
|
|
|
if (i % 20 == 0)
|
|
{
|
|
await SuspendIfOutOfTime();
|
|
|
|
if (!ValidateResume())
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task PostGen(CornerClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
|
|
Random random)
|
|
{
|
|
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
|
var tagQuery = _entManager.GetEntityQuery<TagComponent>();
|
|
|
|
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<DirectionFlag>(4);
|
|
var pocketDirections = new ValueList<Direction>(4);
|
|
var doorQuery = _entManager.GetEntityQuery<DoorComponent>();
|
|
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
|
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<DungeonRoom>(dungeon.Rooms);
|
|
var roomTiles = new List<Vector2i>();
|
|
var tileDef = _tileDefManager[gen.Tile];
|
|
|
|
for (var i = 0; i < gen.Count; i++)
|
|
{
|
|
var roomIndex = random.Next(rooms.Count);
|
|
var room = rooms[roomIndex];
|
|
|
|
// Move out 3 tiles in a direction away from center of the room
|
|
// If none of those intersect another tile it's probably external
|
|
// TODO: Maybe need to take top half of furthest rooms in case there's interior exits?
|
|
roomTiles.AddRange(room.Exterior);
|
|
random.Shuffle(roomTiles);
|
|
|
|
foreach (var tile in roomTiles)
|
|
{
|
|
var isValid = false;
|
|
|
|
// Check if one side is dungeon and the other side is nothing.
|
|
for (var j = 0; j < 4; j++)
|
|
{
|
|
var dir = (Direction) (j * 2);
|
|
var oppositeDir = dir.GetOpposite();
|
|
var dirVec = tile + dir.ToIntVec();
|
|
var oppositeDirVec = tile + oppositeDir.ToIntVec();
|
|
|
|
if (!dungeon.RoomTiles.Contains(dirVec))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (dungeon.RoomTiles.Contains(oppositeDirVec) ||
|
|
dungeon.RoomExteriorTiles.Contains(oppositeDirVec) ||
|
|
dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) ||
|
|
dungeon.CorridorTiles.Contains(oppositeDirVec))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if exterior spot free.
|
|
if (!_anchorable.TileFree(_grid, tile, 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;
|
|
}
|
|
|
|
if (isValid)
|
|
break;
|
|
}
|
|
|
|
roomTiles.Clear();
|
|
}
|
|
}
|
|
|
|
private async Task PostGen(ExternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
|
|
Random random)
|
|
{
|
|
// Iterate every tile with N chance to spawn windows on that wall per cardinal dir.
|
|
var chance = 0.25 / 3f;
|
|
|
|
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
|
|
allExterior.UnionWith(dungeon.RoomExteriorTiles);
|
|
var validTiles = allExterior.ToList();
|
|
random.Shuffle(validTiles);
|
|
|
|
var tiles = new List<(Vector2i, Tile)>();
|
|
var tileDef = _tileDefManager[gen.Tile];
|
|
var count = Math.Floor(validTiles.Count * chance);
|
|
var index = 0;
|
|
var takenTiles = new HashSet<Vector2i>();
|
|
|
|
// There's a bunch of shit here but tl;dr
|
|
// - don't spawn over cap
|
|
// - Check if we have 3 tiles in a row that aren't corners and aren't obstructed
|
|
foreach (var tile in validTiles)
|
|
{
|
|
if (index > count)
|
|
break;
|
|
|
|
// Room tile / already used.
|
|
if (!_anchorable.TileFree(_grid, tile, CollisionLayer, CollisionMask) ||
|
|
takenTiles.Contains(tile))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check we're not on a corner
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
var dir = (Direction) (i * 2);
|
|
var dirVec = dir.ToIntVec();
|
|
var isValid = true;
|
|
|
|
// Check 1 beyond either side to ensure it's not a corner.
|
|
for (var j = -1; j < 4; j++)
|
|
{
|
|
var neighbor = tile + dirVec * j;
|
|
|
|
if (!allExterior.Contains(neighbor) ||
|
|
takenTiles.Contains(neighbor) ||
|
|
!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask))
|
|
{
|
|
isValid = false;
|
|
break;
|
|
}
|
|
|
|
// Also check perpendicular that it is free
|
|
foreach (var k in new [] {2, 6})
|
|
{
|
|
var perp = (Direction) ((i * 2 + k) % 8);
|
|
var perpVec = perp.ToIntVec();
|
|
var perpTile = tile + perpVec;
|
|
|
|
if (allExterior.Contains(perpTile) ||
|
|
takenTiles.Contains(neighbor) ||
|
|
!_anchorable.TileFree(_grid, perpTile, CollisionLayer, CollisionMask))
|
|
{
|
|
isValid = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isValid)
|
|
break;
|
|
}
|
|
|
|
if (!isValid)
|
|
continue;
|
|
|
|
for (var j = 0; j < 3; j++)
|
|
{
|
|
var neighbor = tile + dirVec * j;
|
|
|
|
tiles.Add((neighbor, _tileDefManager.GetVariantTile(tileDef, random)));
|
|
index++;
|
|
takenTiles.Add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
grid.SetTiles(tiles);
|
|
index = 0;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* You may be wondering why these are different.
|
|
* It's because for internals we want to force it as it looks nicer and not leave it up to chance.
|
|
*/
|
|
|
|
// TODO: Can probably combine these a bit, their differences are in really annoying to pull out spots.
|
|
|
|
private async Task PostGen(InternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
|
|
Random random)
|
|
{
|
|
// Iterate every room and check if there's a gap beyond it that leads to another room within N tiles
|
|
// If so then consider windows
|
|
var minDistance = 4;
|
|
var maxDistance = 6;
|
|
var tileDef = _tileDefManager[gen.Tile];
|
|
|
|
foreach (var room in dungeon.Rooms)
|
|
{
|
|
var validTiles = new List<Vector2i>();
|
|
|
|
for (var i = 0; i < 4; i++)
|
|
{
|
|
var dir = (DirectionFlag) Math.Pow(2, i);
|
|
var dirVec = dir.AsDir().ToIntVec();
|
|
|
|
foreach (var tile in room.Tiles)
|
|
{
|
|
var tileAngle = ((Vector2) tile + grid.TileSize / 2f - room.Center).ToAngle();
|
|
var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2);
|
|
|
|
var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded();
|
|
|
|
if (!tileVec.Equals(dirVec))
|
|
continue;
|
|
|
|
var valid = false;
|
|
|
|
for (var j = 1; j < maxDistance; j++)
|
|
{
|
|
var edgeNeighbor = tile + dirVec * j;
|
|
|
|
if (dungeon.RoomTiles.Contains(edgeNeighbor))
|
|
{
|
|
if (j < minDistance)
|
|
{
|
|
valid = false;
|
|
}
|
|
else
|
|
{
|
|
valid = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!valid)
|
|
continue;
|
|
|
|
var windowTile = tile + dirVec;
|
|
|
|
if (!_anchorable.TileFree(grid, windowTile, CollisionLayer, CollisionMask))
|
|
continue;
|
|
|
|
validTiles.Add(windowTile);
|
|
}
|
|
|
|
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++)
|
|
{
|
|
var tile = validTiles[j];
|
|
var gridPos = grid.GridTileToLocal(tile);
|
|
grid.SetTile(tile, _tileDefManager.GetVariantTile(tileDef, random));
|
|
|
|
_entManager.SpawnEntities(gridPos, gen.Entities);
|
|
}
|
|
|
|
if (validTiles.Count > 0)
|
|
{
|
|
await SuspendIfOutOfTime();
|
|
|
|
if (!ValidateResume())
|
|
return;
|
|
}
|
|
|
|
validTiles.Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simply places tiles / entities on the entrances to rooms.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates corridor connections between entrances to all the rooms.
|
|
/// </summary>
|
|
private async Task PostGen(CorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
|
|
{
|
|
var entrances = new List<Vector2i>(dungeon.Rooms.Count);
|
|
|
|
// Grab entrances
|
|
foreach (var room in dungeon.Rooms)
|
|
{
|
|
entrances.AddRange(room.Entrances);
|
|
}
|
|
|
|
var edges = _dungeon.MinimumSpanningTree(entrances, random);
|
|
await 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<Vector2i>();
|
|
|
|
if (expansion >= 1)
|
|
{
|
|
foreach (var tile in dungeon.RoomExteriorTiles)
|
|
{
|
|
for (var x = -expansion; x <= expansion; x++)
|
|
{
|
|
for (var y = -expansion; y <= expansion; y++)
|
|
{
|
|
var neighbor = new 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<Vector2i>(dungeon.RoomExteriorTiles);
|
|
excludedTiles.UnionWith(dungeon.RoomTiles);
|
|
var corridorTiles = new HashSet<Vector2i>();
|
|
|
|
_dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile =>
|
|
{
|
|
var mod = 1f;
|
|
|
|
if (corridorTiles.Contains(tile))
|
|
{
|
|
mod *= 0.1f;
|
|
}
|
|
|
|
if (deterredTiles.Contains(tile))
|
|
{
|
|
mod *= 2f;
|
|
}
|
|
|
|
return mod;
|
|
});
|
|
|
|
// Widen the path
|
|
if (expansion >= 1)
|
|
{
|
|
var toAdd = new ValueList<Vector2i>();
|
|
|
|
foreach (var node in corridorTiles)
|
|
{
|
|
// Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug
|
|
// exterior walls and make the path smaller.
|
|
|
|
for (var x = -expansion; x <= expansion; x++)
|
|
{
|
|
for (var y = -expansion; y <= expansion; y++)
|
|
{
|
|
var neighbor = new 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<Vector2i>(dungeon.Rooms.Count);
|
|
|
|
foreach (var room in dungeon.Rooms)
|
|
{
|
|
foreach (var entrance in room.Entrances)
|
|
{
|
|
for (var i = 0; i < 8; i++)
|
|
{
|
|
var dir = (Direction) i;
|
|
var neighbor = entrance + dir.ToIntVec();
|
|
|
|
if (!dungeon.RoomExteriorTiles.Contains(neighbor))
|
|
continue;
|
|
|
|
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
|
|
|
|
// Grab all of the room bounds
|
|
// Then, work out connections between them
|
|
var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
|
|
|
|
foreach (var room in dungeon.Rooms)
|
|
{
|
|
var roomEdges = new HashSet<Vector2i>();
|
|
|
|
foreach (var index in room.Tiles)
|
|
{
|
|
for (var x = -1; x <= 1; x++)
|
|
{
|
|
for (var y = -1; y <= 1; y++)
|
|
{
|
|
// Cardinals only
|
|
if (x != 0 && y != 0 ||
|
|
x == 0 && y == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var neighbor = new Vector2i(index.X + x, index.Y + y);
|
|
|
|
if (dungeon.RoomTiles.Contains(neighbor))
|
|
continue;
|
|
|
|
if (!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask))
|
|
continue;
|
|
|
|
roomEdges.Add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
roomBorders.Add(room, roomEdges);
|
|
}
|
|
|
|
// Do pathfind from first room to work out graph.
|
|
// TODO: Optional loops
|
|
|
|
var roomConnections = new Dictionary<DungeonRoom, List<DungeonRoom>>();
|
|
var frontier = new Queue<DungeonRoom>();
|
|
frontier.Enqueue(dungeon.Rooms.First());
|
|
var tileDef = _tileDefManager[gen.Tile];
|
|
|
|
foreach (var (room, border) in roomBorders)
|
|
{
|
|
var conns = roomConnections.GetOrNew(room);
|
|
|
|
foreach (var (otherRoom, otherBorders) in roomBorders)
|
|
{
|
|
if (room.Equals(otherRoom) ||
|
|
conns.Contains(otherRoom))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var flipp = new HashSet<Vector2i>(border);
|
|
flipp.IntersectWith(otherBorders);
|
|
|
|
if (flipp.Count == 0 ||
|
|
gen.OverlapCount != -1 && flipp.Count != gen.OverlapCount)
|
|
continue;
|
|
|
|
var center = Vector2.Zero;
|
|
|
|
foreach (var node in flipp)
|
|
{
|
|
center += (Vector2) node + grid.TileSize / 2f;
|
|
}
|
|
|
|
center /= flipp.Count;
|
|
// Weight airlocks towards center more.
|
|
var nodeDistances = new List<(Vector2i Node, float Distance)>(flipp.Count);
|
|
|
|
foreach (var node in flipp)
|
|
{
|
|
nodeDistances.Add((node, ((Vector2) node + grid.TileSize / 2f - center).LengthSquared));
|
|
}
|
|
|
|
nodeDistances.Sort((x, y) => x.Distance.CompareTo(y.Distance));
|
|
|
|
var width = gen.Count;
|
|
|
|
for (var i = 0; i < nodeDistances.Count; i++)
|
|
{
|
|
var node = nodeDistances[i].Node;
|
|
var gridPos = grid.GridTileToLocal(node);
|
|
if (!_anchorable.TileFree(grid, node, CollisionLayer, CollisionMask))
|
|
continue;
|
|
|
|
width--;
|
|
grid.SetTile(node, _tileDefManager.GetVariantTile(tileDef, random));
|
|
|
|
if (gen.EdgeEntities != null && nodeDistances.Count - i <= 2)
|
|
{
|
|
_entManager.SpawnEntities(gridPos, gen.EdgeEntities);
|
|
}
|
|
else
|
|
{
|
|
// Iterate neighbors and check for blockers, if so bulldoze
|
|
ClearDoor(dungeon, grid, node);
|
|
|
|
_entManager.SpawnEntities(gridPos, gen.Entities);
|
|
}
|
|
|
|
if (width == 0)
|
|
break;
|
|
}
|
|
|
|
conns.Add(otherRoom);
|
|
var otherConns = roomConnections.GetOrNew(otherRoom);
|
|
otherConns.Add(room);
|
|
await SuspendIfOutOfTime();
|
|
|
|
if (!ValidateResume())
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes any unwanted obstacles around a door tile.
|
|
/// </summary>
|
|
private void ClearDoor(Dungeon dungeon, MapGridComponent grid, Vector2i indices, bool strict = false)
|
|
{
|
|
var flags = strict
|
|
? LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries
|
|
: LookupFlags.Dynamic | LookupFlags.Static;
|
|
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
|
|
|
for (var x = -1; x <= 1; x++)
|
|
{
|
|
for (var y = -1; y <= 1; y++)
|
|
{
|
|
if (x != 0 && y != 0)
|
|
continue;
|
|
|
|
var neighbor = new Vector2i(indices.X + x, indices.Y + y);
|
|
|
|
if (!dungeon.RoomTiles.Contains(neighbor))
|
|
continue;
|
|
|
|
// Shrink by 0.01 to avoid polygon overlap from neighboring tiles.
|
|
foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, new Box2(neighbor * grid.TileSize, (neighbor + 1) * grid.TileSize).Enlarged(-0.1f), flags))
|
|
{
|
|
if (!physicsQuery.TryGetComponent(ent, out var physics) ||
|
|
!physics.Hard ||
|
|
(CollisionMask & physics.CollisionLayer) == 0x0 &&
|
|
(CollisionLayer & physics.CollisionMask) == 0x0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_entManager.DeleteEntity(ent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task PostGen(WallMountPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
|
|
Random random)
|
|
{
|
|
var tileDef = _tileDefManager[gen.Tile];
|
|
var checkedTiles = new HashSet<Vector2i>();
|
|
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
|
|
allExterior.UnionWith(dungeon.RoomExteriorTiles);
|
|
var count = 0;
|
|
|
|
foreach (var neighbor in allExterior)
|
|
{
|
|
// Occupado
|
|
if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(grid, neighbor, 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)
|
|
{
|
|
count -= 20;
|
|
await SuspendIfOutOfTime();
|
|
|
|
if (!ValidateResume())
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|