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(MaxRooms * 4).ToList(); var randomOffshootRoomsList = OffshootRooms.Shuffle(DungeonLength * 4).ToList(); 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}"); 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); if (spawnedRoom is null) { continue; } 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()]; 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(); var res = SpawnOffshoot(lastRoom, direction, offshootTypeToSpawn, roomsInOffshot, roomsForOffshoot); 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 && shuffledOffshoots[currentOffshoot % shuffledOffshoots.Count()] 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 spawnedBossRoom = TrySpawnRoom(BossRooms.ToList(), 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(List 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(); for (int i = 0; i < rooms.Count(); i++) { var roomToSpawn = rooms[i % rooms.Count()]; var spawnedRoom = TrySpawnRoom(roomToSpawn, nextPos, direction); if (spawnedRoom is null) { GD.PrintErr($"Could not spawn room {roomToSpawn} at {nextPos}"); 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, List randomOffshootRoomsList) { RogueliteRoom lastSpawnedOffshootRoom = lastRoom; 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(MapTheme.Rooms.Where(x => x.Type == offshootTypeToSpawn).ToList(), 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); } } }