using System; using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Actors; using Cirno.Scripts.Enums; using Cirno.Scripts.Resources; using Cirno.Scripts.Resources.Roguelite; using Cirno.Scripts.Utils; using Godot; using Godot.Collections; using Range = System.Range; namespace Cirno.Scripts.Controllers; public partial class RogueliteRoomManager : Node2D { //[Export] public Array Rooms { get; set; } [Export] public RogueliteMapTheme MapTheme { get; set; } //private Godot.Collections.Dictionary _grid = new(); private Godot.Collections.Dictionary _roomGrid = new(); public Godot.Collections.Dictionary RoomGrid => _roomGrid; public List SpawnedRooms { get; private set; } = []; [Export] public Vector2I SpawnOrigin { get; private set; } = Vector2I.Zero; [Export] public int DungeonLength { get; set; } = 10; [Export] public int MaxBranches { get; set; } = 3; [Export] public int MaxBranchLength { get; set; } = 3; [Export] public int DungeonWidth = 10; [Export] public int DungeonHeight = 10; [Export] public int MaxRooms = 12; [Export] public int MinKeys = 0; [Export] public int MaxKeys = 3; [Export] public int MinSecrets = 1; [Export] public int MaxSecrets = 2; [Export] public int MinTreasures = 1; [Export] public int MaxTreasures = 2; [Export] public int MinShops = 1; [Export] public int MaxShops = 1; [Export] public string ManualSeed { get; private set; } private ulong _seed; [Export] public Vector2I TileSize { get; set; } = new Vector2I(16, 16); [Export] public Vector2I RoomSizeInTiles { get; set; } = new Vector2I(20, 10); [Signal] public delegate void MapCreatedEventHandler(); public override void _Ready() { // Spawn first room } public void InitSpawning() { GenerateStraightLineDungeon(); } private void SpawnRoomsGrid() { //var firstRoom = Rooms.FirstOrDefault(); var origin = Vector2.Zero; var tileSize = new Vector2(16, 16); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { var roomIndex = GD.RandRange(0, MapTheme.Rooms.Count - 1); var room = MapTheme.Rooms[roomIndex]; //SpawnRoom(room, origin + (room.Size * new Vector2(i, j) * tileSize)); } } //CallDeferred(MethodName.RebakeNavigationDeferred); } private IEnumerable StarterRooms => MapTheme.Rooms.Where(x => x.Type is RoomType.Starter); private IEnumerable RegularRooms => MapTheme.Rooms.Where(x => x.Type is RoomType.Regular && x.HasDoors(DoorDirections.North | DoorDirections.South)); private IEnumerable OffshootRooms => MapTheme.Rooms.Where(x => x.Type is RoomType.Regular && x.HasDoors(DoorDirections.East | DoorDirections.West)); private IEnumerable BossRooms => MapTheme.Rooms.Where(x => x.Type is RoomType.Boss); private List _connections = new(); public List Connections => _connections; private void SetSeed() { if (!string.IsNullOrWhiteSpace(ManualSeed) && ulong.TryParse(ManualSeed, out var seed)) { _seed = seed; } else { var rng = new RandomNumberGenerator(); _seed = rng.GetSeed(); rng.Dispose(); } GD.Print($"Seed: {_seed}"); } private void GenerateStraightLineDungeon() { SetSeed(); MapTheme.MakeChestLootQueue(); MapTheme.TeleportersList = []; Vector2I origin = Vector2I.Zero; var starterRoom = MapTheme.Rooms.Where(r => r.Type == RoomType.Starter).PickRandom(); SpawnRoom(starterRoom, origin, out var spawnedBeginRoom); var randomRoomsList = RegularRooms.Shuffle().ToList(); var regularRoomsQueue = new Queue().EnqueueRange(randomRoomsList); var randomOffshootRoomsList = OffshootRooms.Shuffle().ToList(); var offshootRoomsQueue = new Queue().EnqueueRange(randomOffshootRoomsList); var currentPos = spawnedBeginRoom.RandomBottomExit(); _connections.Add(new RoomConnection(origin, currentPos + new Vector2I(0, 1))); Vector2I nextPos; var offshoots = new List(); AddRandomOffshootType(offshoots, MinKeys, MaxKeys, RoomType.Key); AddRandomOffshootType(offshoots, MinShops, MaxShops, RoomType.Shop); AddRandomOffshootType(offshoots, MinSecrets, MaxSecrets, RoomType.Secret); AddRandomOffshootType(offshoots, MinTreasures, MaxTreasures, RoomType.Treasure); var shuffledOffshoots = offshoots.Shuffle().ToList(); var offshootsQueue = new Queue(); offshootsQueue.EnqueueRange(shuffledOffshoots); int currentOffshoot = 0; RogueliteRoom lastRoom = spawnedBeginRoom; bool lockNext = false; for (int i = 0; i < DungeonLength; i++) { GD.Print($"Dungeon room {i}"); // if (!regularRoomsQueue.TryDequeue(out var roomToSpawn)) // { // GD.Print("Ran out of regular rooms, add more"); // return; // } // var randRoomStartIndex = SpawnedRooms.Count(x => // x.RoomResource.Type is RoomType.Regular && // x.RoomResource.HasDoors(DoorDirections.North | DoorDirections.South)); // var spawnedRoom = TrySpawnRoom( // randomRoomsList.Take(new Range(randRoomStartIndex, randomRoomsList.Count - 1)).ToList(), // lastRoom, Direction.Down); var spawnedRoom = TrySpawnRoom(regularRoomsQueue, lastRoom, Direction.Down); if (spawnedRoom is null) { GD.Print("Abort creation"); return; } if (lockNext) { _connections.Last().IsLocked = true; lockNext = false; } lastRoom = spawnedRoom; // Spawn offshoot here // Roll whether to go left or right, if direction is full go the other, if both are full do not spawn var directions = new List(); if (lastRoom.RoomResource.DoorDirections.HasFlag(DoorDirections.East)) directions.Add(Direction.Right); if (lastRoom.RoomResource.DoorDirections.HasFlag(DoorDirections.West)) directions.Add(Direction.Left); directions = directions.Shuffle().ToList(); foreach (var direction in directions) { // var randOffshootStartIndex = SpawnedRooms.Count(x => // x.RoomResource.Type is RoomType.Regular && // x.RoomResource.HasDoors(DoorDirections.West | DoorDirections.East)); //var offshootTypeToSpawn = shuffledOffshoots[currentOffshoot % shuffledOffshoots.Count()]; if (!offshootsQueue.TryDequeue(out var offshootTypeToSpawn)) { GD.Print("Ran out of offshoot types, add more"); break; } int roomsInOffshot = offshootTypeToSpawn is RoomType.Secret or RoomType.Shop ? 0 : GD.RandRange(0, MaxBranchLength); // var roomsForOffshoot = randomOffshootRoomsList // .Take(new Range(randOffshootStartIndex, randomOffshootRoomsList.Count - 1)).ToList(); // This one reshuffles improperly, good for now var endRooms = new Queue().EnqueueRange(MapTheme.Rooms .Where(x => x.Type == offshootTypeToSpawn).ToList()); var res = SpawnOffshoot(lastRoom, direction, offshootTypeToSpawn, roomsInOffshot, offshootRoomsQueue, endRooms); if (res) { currentOffshoot++; // Try to spawn one the other direction too // If key, lock the last connection if (offshootTypeToSpawn is RoomType.Key) { //lastConnection.IsLocked = true; // Need to lock the next connection lockNext = true; } if (offshootTypeToSpawn is RoomType.Key && offshootsQueue.Peek() is RoomType.Key) { // Stop if next room is a key break; } //break; } } // Add more dungeon if not enough rooms are generated if (i == DungeonLength - 1 && DungeonLength < MaxRooms && currentOffshoot < DungeonLength) { DungeonLength++; } } var bossRoom = BossRooms.PickRandom(); var bossQueue = new Queue().EnqueueRange(BossRooms.Shuffle()); var spawnedBossRoom = TrySpawnRoom(bossQueue, lastRoom, Direction.Down); if (spawnedBossRoom is null) { GD.PrintErr($"Could not spawn boss room {bossRoom}"); } if (lockNext) { _connections.Last().IsLocked = true; lockNext = false; } foreach (var room in SpawnedRooms) { room.HandleDoors((doorEdge, pos) => { //var neighborPos = room.GridPosition + pos; return _connections.FirstOrDefault(x => (x.From == doorEdge && x.To == pos) || (x.From == pos && x.To == doorEdge)); //return _roomGrid.ContainsKey(neighborPos); }); } // Debug teleporter matching MatchBossTeleporter(); EmitSignalMapCreated(); //CallDeferred(MethodName.OpenStartDoorsDeferred, spawnedBeginRoom); } private void MatchBossTeleporter() { var bossTeleporters = MapTheme.TeleportersList.Where(x => x.Type is TeleporterMarkerType.Boss); foreach (var teleporterMarker in bossTeleporters) { var bossRoom = SpawnedRooms.FirstOrDefault(x => x.RoomResource.Type is RoomType.Boss); if (bossRoom is null) return; var teleporter = bossRoom.Teleporters.FirstOrDefault(x => x.Type is TeleporterMarkerType.Receiver or TeleporterMarkerType.InvisibleReceiver); if (teleporter is null) return; teleporterMarker.SpawnedTeleporter.Target = teleporter.SpawnedTeleporter; } } private RogueliteRoom TrySpawnRoom(Queue roomsList, RogueliteRoom lastRoom, Direction direction) { //var nextPos = originPos + new Vector2I(0, 1); Vector2I exitPosition = lastRoom.RandomExit(direction); Vector2I nextPos = direction switch { Direction.Up => exitPosition + Vector2I.Up, Direction.Down => exitPosition + Vector2I.Down, Direction.Left => exitPosition + Vector2I.Left, Direction.Right => exitPosition + Vector2I.Right, _ => exitPosition }; //var rooms = roomsList; //.Shuffle().ToList(); int tries = 0; while (roomsList.Count > 0 && tries < 10) { if (!roomsList.TryDequeue(out var roomToSpawn)) { GD.Print("Ran out of regular rooms, add more"); return null; } var spawnedRoom = TrySpawnRoom(roomToSpawn, nextPos, direction); if (spawnedRoom is null) { GD.PrintErr($"Could not spawn room {roomToSpawn} at {nextPos}"); roomsList.Enqueue(roomToSpawn); tries++; continue; } _connections.Add(new RoomConnection(exitPosition, nextPos)); return spawnedRoom; } GD.PrintErr($"Could not spawn room at all at {nextPos}"); return null; } private readonly System.Collections.Generic.Dictionary dirDict = new() { { Direction.Up, Vector2I.Up }, { Direction.Down, Vector2I.Down }, { Direction.Left, Vector2I.Left }, { Direction.Right, Vector2I.Right }, }; private RogueliteRoom TrySpawnRoom(RogueliteRoomResource roomToSpawn, Vector2I nextPos, Direction direction) { List offsets = []; offsets.AddRange(roomToSpawn.GetRoomOffsets(nextPos, direction)); foreach (var offset in offsets) { if (!SpawnRoom(roomToSpawn, offset, out var spawnedRoom)) { GD.PrintErr($"Could not spawn room {roomToSpawn} at {nextPos}"); //DungeonLength += 1; } else { return spawnedRoom; } } return null; } private Vector2I GetNextPosition(Vector2I current, Vector2I target) { var possibleDirs = new List(); if (target.X > current.X) possibleDirs.Add(Vector2I.Right); else if (target.X < current.X) possibleDirs.Add(Vector2I.Left); if (target.Y > current.Y) possibleDirs.Add(Vector2I.Down); else if (target.Y < current.Y) possibleDirs.Add(Vector2I.Up); // Add random noise to allow slightly winding paths possibleDirs.AddRange(new[] { Vector2I.Right, Vector2I.Left, Vector2I.Up, Vector2I.Down }); possibleDirs = possibleDirs.Distinct().OrderBy(_ => GD.Randi()).ToList(); foreach (var dir in possibleDirs) { var nextPos = current + dir; if (!_roomGrid.ContainsKey(nextPos)) return nextPos; } return current; // No valid next position found } private void RebakeNavigationDeferred() { GameManager.Instance.RebakeNavigation(); } private bool SpawnOffshoot(RogueliteRoom lastRoom, Direction direction, RoomType offshootTypeToSpawn, int roomsInOffshot, Queue randomOffshootRoomsList, Queue endRoomsQueue) { RogueliteRoom lastSpawnedOffshootRoom = lastRoom; int tries = 0; int spawned = 0; while (randomOffshootRoomsList.Count > 0 && tries < 10 && spawned < roomsInOffshot) { var spawnedRoom = TrySpawnRoom(randomOffshootRoomsList, lastSpawnedOffshootRoom, direction); if (spawnedRoom is null) { // GD.Print($"Could not place offshoot {shuffledOffshoot}"); // Try next in list tries++; continue; } lastSpawnedOffshootRoom = spawnedRoom; //roomsInOffshot++; spawned++; } if (lastSpawnedOffshootRoom is null) { return false; } // for (int j = 0; j < roomsInOffshot; j++) // { // var shuffledOffshootRoomsList = randomOffshootRoomsList; //.Shuffle().ToList(); // // foreach (var shuffledOffshoot in shuffledOffshootRoomsList) // { // var spawnedRoom = TrySpawnRoom(shuffledOffshootRoomsList, lastSpawnedOffshootRoom, direction); // // if (spawnedRoom is null) // { // GD.Print($"Could not place offshoot {shuffledOffshoot}"); // // Try next in list // continue; // } // // lastSpawnedOffshootRoom = spawnedRoom; // break; // } // // // Nope no offshoot // if (lastSpawnedOffshootRoom is null) // { // return false; // } // } // Offshoot over // Spawn final room // var finalRoomToSpawn = Rooms.Where(x => x.Type == offshootTypeToSpawn).PickRandom(); var spawnedFinalRoom = TrySpawnRoom(endRoomsQueue, lastSpawnedOffshootRoom, direction); if (spawnedFinalRoom is null) { GD.Print($"Could not place offshoot final room {lastSpawnedOffshootRoom}"); // Try next in list return false; } // var finalRoomCoord = leftPosition - new Vector2I(finalRoomToSpawn.Size.X, 0); // // SpawnRoom(finalRoomToSpawn, finalRoomCoord, out var spawnedFinalRoom); // // _connections.Add(new RoomConnection(leftPosition, // finalRoomCoord + new Vector2I(finalRoomToSpawn.Size.X - 1, 0))); return true; } private bool SpawnRoom(RogueliteRoomResource room, Vector2I gridPos, out RogueliteRoom spawnedRoom) { if (!CanPlaceRoom(gridPos, room.Size)) { spawnedRoom = null; GD.Print($"{gridPos} size {room.Size} is occupied"); return false; } var position = gridPos * TileSize * RoomSizeInTiles; var roomScene = GD.Load(room.ScenePath); var spawnedScene = this.CreateChild(roomScene, position); spawnedScene.MapTheme = MapTheme; spawnedScene.GridPosition = gridPos; spawnedScene.Name = room.RoomName.ToString().Replace(" ", "_"); SpawnedRooms.Add(spawnedScene); GD.Print($"Spawned room: {spawnedScene}"); // for reference //SpawnRoom(room, origin + (room.Size * new Vector2(i, j) * tileSize)); spawnedScene.Spawn(); MarkRoom(gridPos, room.Size, spawnedScene); spawnedRoom = spawnedScene; return true; } // private bool CheckConnection(Vector2I pos) // { // return _grid.ContainsKey(pos); // <- this is your dungeon grid dictionary // } // private bool CanPlaceRoom(Vector2I origin, Vector2I size) { for (int x = 0; x < size.X; x++) { for (int y = 0; y < size.Y; y++) { var pos = origin + new Vector2I(x, y); if (_roomGrid.ContainsKey(pos)) return false; } } return true; } private void MarkRoom(Vector2I origin, Vector2I size, RogueliteRoom room) { for (int x = 0; x < size.X; x++) { for (int y = 0; y < size.Y; y++) { var pos = origin + new Vector2I(x, y); _roomGrid[pos] = room; } } } private void AddRandomOffshootType(List roomTypes, int minAmount, int maxAmount, RoomType type) { var itemsToAdd = GD.RandRange(minAmount, maxAmount); GD.Print($"Adding {itemsToAdd} {type}"); for (int k = 0; k < itemsToAdd; k++) { roomTypes.Add(type); } } }