* Add CP14RoomsDunGen for procedural room placement Introduces the CP14RoomsDunGen layer for generating rooms across the grid based on tags and tile masks. Updates DungeonJob and DungeonSystem to support the new layer, and modifies relevant YAML prototypes to use CP14RoomsDunGen instead of OreDunGen for room spawning in demiplane modifiers. * some fixes * its done done done * exit room * additional entry points * fix * Update migration.yml
270 lines
9.8 KiB
C#
270 lines
9.8 KiB
C#
using System.Numerics;
|
|
using Content.Shared.Decals;
|
|
using Content.Shared.Maps;
|
|
using Content.Shared.Procedural;
|
|
using Content.Shared.Random.Helpers;
|
|
using Content.Shared.Whitelist;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Procedural;
|
|
|
|
public sealed partial class DungeonSystem
|
|
{
|
|
// Temporary caches.
|
|
private readonly HashSet<EntityUid> _entitySet = new();
|
|
private readonly List<DungeonRoomPrototype> _availableRooms = new();
|
|
|
|
/// <summary>
|
|
/// Gets a random dungeon room matching the specified area, whitelist and size.
|
|
/// </summary>
|
|
public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, Random random, EntityWhitelist? whitelist = null)
|
|
{
|
|
return GetRoomPrototype(random, whitelist, minSize: size, maxSize: size);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a random dungeon room matching the specified area and whitelist and size range
|
|
/// </summary>
|
|
public DungeonRoomPrototype? GetRoomPrototype(Random random,
|
|
EntityWhitelist? whitelist = null,
|
|
Vector2i? minSize = null,
|
|
Vector2i? maxSize = null)
|
|
{
|
|
// Can never be true.
|
|
if (whitelist is { Tags: null })
|
|
{
|
|
return null;
|
|
}
|
|
|
|
_availableRooms.Clear();
|
|
|
|
foreach (var proto in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
|
|
{
|
|
if (minSize is not null && (proto.Size.X < minSize.Value.X || proto.Size.Y < minSize.Value.Y))
|
|
continue;
|
|
|
|
if (maxSize is not null && (proto.Size.X > maxSize.Value.X || proto.Size.Y > maxSize.Value.Y))
|
|
continue;
|
|
|
|
if (whitelist == null)
|
|
{
|
|
_availableRooms.Add(proto);
|
|
continue;
|
|
}
|
|
|
|
foreach (var tag in whitelist.Tags)
|
|
{
|
|
if (!proto.Tags.Contains(tag))
|
|
continue;
|
|
|
|
_availableRooms.Add(proto);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_availableRooms.Count == 0)
|
|
return null;
|
|
|
|
var room = _availableRooms[random.Next(_availableRooms.Count)];
|
|
|
|
return room;
|
|
}
|
|
|
|
public void SpawnRoom(
|
|
EntityUid gridUid,
|
|
MapGridComponent grid,
|
|
Vector2i origin,
|
|
DungeonRoomPrototype room,
|
|
Random random,
|
|
HashSet<Vector2i>? reservedTiles = null, //CP14 default null
|
|
bool clearExisting = false,
|
|
bool rotation = false)
|
|
{
|
|
var originTransform = Matrix3Helpers.CreateTranslation(origin.X, origin.Y);
|
|
var roomRotation = Angle.Zero;
|
|
|
|
if (rotation)
|
|
{
|
|
roomRotation = GetRoomRotation(room, random);
|
|
}
|
|
|
|
var roomTransform = Matrix3Helpers.CreateTransform((Vector2)room.Size / 2f, roomRotation);
|
|
var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform);
|
|
|
|
SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting);
|
|
}
|
|
|
|
public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
|
|
{
|
|
var roomRotation = Angle.Zero;
|
|
|
|
if (room.Size.X == room.Size.Y)
|
|
{
|
|
// Give it a random rotation
|
|
roomRotation = random.Next(4) * Math.PI / 2;
|
|
}
|
|
else if (random.Next(2) == 1)
|
|
{
|
|
roomRotation += Math.PI;
|
|
}
|
|
|
|
return roomRotation;
|
|
}
|
|
|
|
public void SpawnRoom(
|
|
EntityUid gridUid,
|
|
MapGridComponent grid,
|
|
Matrix3x2 roomTransform,
|
|
DungeonRoomPrototype room,
|
|
HashSet<Vector2i>? reservedTiles = null,
|
|
bool clearExisting = false)
|
|
{
|
|
// Ensure the underlying template exists.
|
|
var roomMap = GetOrCreateTemplate(room);
|
|
var templateMapUid = _maps.GetMapOrInvalid(roomMap);
|
|
var templateGrid = Comp<MapGridComponent>(templateMapUid);
|
|
var roomDimensions = room.Size;
|
|
|
|
var finalRoomRotation = roomTransform.Rotation();
|
|
|
|
var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
|
|
var tileOffset = -roomCenter + grid.TileSizeHalfVector;
|
|
_tiles.Clear();
|
|
|
|
// Load tiles
|
|
for (var x = 0; x < roomDimensions.X; x++)
|
|
{
|
|
for (var y = 0; y < roomDimensions.Y; y++)
|
|
{
|
|
var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
|
|
var tileRef = _maps.GetTileRef(templateMapUid, templateGrid, indices);
|
|
|
|
var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
|
|
var rounded = tilePos.Floored();
|
|
|
|
if (!clearExisting && reservedTiles?.Contains(rounded) == true)
|
|
continue;
|
|
|
|
if (room.IgnoreTile is not null)
|
|
{
|
|
if (_maps.TryGetTileDef(templateGrid, indices, out var tileDef) && room.IgnoreTile == tileDef.ID)
|
|
continue;
|
|
}
|
|
|
|
_tiles.Add((rounded, tileRef.Tile));
|
|
|
|
if (clearExisting)
|
|
{
|
|
var anchored = _maps.GetAnchoredEntities((gridUid, grid), rounded);
|
|
foreach (var ent in anchored)
|
|
{
|
|
QueueDel(ent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var bounds = new Box2(room.Offset, room.Offset + room.Size);
|
|
|
|
_maps.SetTiles(gridUid, grid, _tiles);
|
|
|
|
// Load entities
|
|
// TODO: I don't think engine supports full entity copying so we do this piece of shit.
|
|
|
|
foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
|
|
{
|
|
var templateXform = _xformQuery.GetComponent(templateEnt);
|
|
var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
|
|
|
|
if (!clearExisting && reservedTiles?.Contains(childPos.Floored()) == true)
|
|
continue;
|
|
|
|
var childRot = templateXform.LocalRotation + finalRoomRotation;
|
|
var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
|
|
|
|
// TODO: Copy the templated entity as is with serv
|
|
var ent = Spawn(protoId, new EntityCoordinates(gridUid, childPos));
|
|
|
|
var childXform = _xformQuery.GetComponent(ent);
|
|
var anchored = templateXform.Anchored;
|
|
_transform.SetLocalRotation(ent, childRot, childXform);
|
|
|
|
// If the templated entity was anchored then anchor us too.
|
|
if (anchored && !childXform.Anchored)
|
|
_transform.AnchorEntity((ent, childXform), (gridUid, grid));
|
|
else if (!anchored && childXform.Anchored)
|
|
_transform.Unanchor(ent, childXform);
|
|
}
|
|
|
|
// Load decals
|
|
if (TryComp<DecalGridComponent>(templateMapUid, out var loadedDecals))
|
|
{
|
|
EnsureComp<DecalGridComponent>(gridUid);
|
|
|
|
foreach (var (_, decal) in _decals.GetDecalsIntersecting(templateMapUid, bounds, loadedDecals))
|
|
{
|
|
// Offset by 0.5 because decals are offset from bot-left corner
|
|
// So we convert it to center of tile then convert it back again after transform.
|
|
// Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
|
|
var position = Vector2.Transform(decal.Coordinates + grid.TileSizeHalfVector - roomCenter, roomTransform);
|
|
position -= grid.TileSizeHalfVector;
|
|
|
|
if (!clearExisting && reservedTiles?.Contains(position.Floored()) == true)
|
|
continue;
|
|
|
|
// Umm uhh I love decals so uhhhh idk what to do about this
|
|
var angle = (decal.Angle + finalRoomRotation).Reduced();
|
|
|
|
// Adjust because 32x32 so we can't rotate cleanly
|
|
// Yeah idk about the uhh vectors here but it looked visually okay but they may still be off by 1.
|
|
// Also EyeManager.PixelsPerMeter should really be in shared.
|
|
if (angle.Equals(Math.PI))
|
|
{
|
|
position += new Vector2(-1f / 32f, 1f / 32f);
|
|
}
|
|
else if (angle.Equals(-Math.PI / 2f))
|
|
{
|
|
position += new Vector2(-1f / 32f, 0f);
|
|
}
|
|
else if (angle.Equals(Math.PI / 2f))
|
|
{
|
|
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();
|
|
|
|
// Fallback because uhhhhhhhh yeah, a corner tile might look valid on the original
|
|
// but place 1 nanometre off grid and fail the add.
|
|
if (!_maps.TryGetTileRef(gridUid, grid, tilePos, out var tileRef) || tileRef.Tile.IsEmpty)
|
|
{
|
|
_maps.SetTile(gridUid, grid, tilePos, _tile.GetVariantTile((ContentTileDefinition)_tileDefManager[FallbackTileId], _random.GetRandom()));
|
|
}
|
|
|
|
var result = _decals.TryAddDecal(
|
|
decal.Id,
|
|
new EntityCoordinates(gridUid, position),
|
|
out _,
|
|
decal.Color,
|
|
angle,
|
|
decal.ZIndex,
|
|
decal.Cleanable);
|
|
|
|
DebugTools.Assert(result);
|
|
}
|
|
}
|
|
}
|
|
}
|