using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Cirno.Scripts.Activables; using Cirno.Scripts.Actors; using Cirno.Scripts.Components.FSM.Enemy; using Cirno.Scripts.Enums; using Cirno.Scripts.Interactables; using Cirno.Scripts.Resources; using Cirno.Scripts.Resources.Loot; using Cirno.Scripts.Resources.Roguelite; using Cirno.Scripts.Utils; using Godot; using Godot.Collections; using Array = Godot.Collections.Array; namespace Cirno.Scripts.Controllers; [Tool] public partial class RogueliteRoom : Node2D { [Export] public RogueliteRoomResource RoomResource { get; set; } [Export] public Array RoomClearActivation { get; set; } public RogueliteMapTheme MapTheme { get; set; } public Vector2I GridPosition { get; set; } // Set by dungeon manager public Vector2I BottomLeft => GridPosition + new Vector2I(0, RoomResource.Size.Y - 1); //private Vector2 BaseRoomSize => new Vector2(320, 160); private Vector2 BaseRoomSize => MapTheme.TileSize * MapTheme.RoomSizeInTiles; public Vector2 RoomSize => BaseRoomSize * RoomResource.Size; [Signal] public delegate void RoomClearedEventHandler(); public Vector2I RandomBottomExit() { return BottomLeft + new Vector2I(GD.RandRange(0, RoomResource.Size.X - 1), 0); } public Vector2I RandomExit(Direction direction) { return direction switch { Direction.Up => GridPosition + new Vector2I(GD.RandRange(0, RoomResource.Size.X - 1), 0), Direction.Down => BottomLeft + new Vector2I(GD.RandRange(0, RoomResource.Size.X - 1), 0), Direction.Left => GridPosition + new Vector2I(0, GD.RandRange(0, RoomResource.Size.Y - 1)), Direction.Right => GridPosition + new Vector2I(RoomResource.Size.X - 1, 0) + new Vector2I(0, GD.RandRange(0, RoomResource.Size.Y - 1)), _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) }; } private static readonly Godot.Collections.Dictionary DirectionMap = new() { { "North", new Vector2I(0, -1) }, { "South", new Vector2I(0, 1) }, { "East", new Vector2I(1, 0) }, { "West", new Vector2I(-1, 0) }, }; private List _doors = []; private List _connections = []; private List _enemies = []; public List Teleporters { get; private set; } = []; private Array SpawnableEnemies => RoomResource.SpawnableEnemies; private BlackCover _shroud; private bool _firstEnter = true; public RogueliteRoom Spawn() { //SpawnEnemies(); SpawnFeatures(); SpawnFixedWeapons(); SpawnShroud(); //HandleDoors(connectionChecker); //AddDebugLabel(); return this; } private void AddDebugLabel() { for (int i = 0; i < RoomResource.Size.X; i++) { for (int j = 0; j < RoomResource.Size.Y; j++) { var label = new Label(); label.Text = $"{GridPosition + new Vector2I(i, j)}"; label.ZIndex = 11; label.Position = (new Vector2(i, j) * new Vector2(320, 160)) + new Vector2(160, 80); this.AddChild(label); } } } private List GenerateDoors() { List doors = []; var doorsContainer = new Node2D(); this.AddChild(doorsContainer); doorsContainer.Name = "Doors"; // North for (int i = 0; i < RoomResource.Size.X; i++) { if (RoomResource.HasDoors(DoorDirections.North)) { doors.Add(MakeDoorMarker(DoorDirections.North, i)); } if (RoomResource.HasDoors(DoorDirections.South)) { doors.Add(MakeDoorMarker(DoorDirections.South, i)); } } for (int j = 0; j < RoomResource.Size.Y; j++) { doors.Add(MakeDoorMarker(DoorDirections.East, j)); doors.Add(MakeDoorMarker(DoorDirections.West, j)); } foreach (var door in doors) { doorsContainer.AddChild(door); } return doors; } private DoorMarker MakeDoorMarker(DoorDirections direction, int wallIndex) { var doorMarker = new DoorMarker(); doorMarker.Direction = direction; doorMarker.WallIndex = wallIndex; doorMarker.Position = GetDoorPosition(direction, wallIndex); return doorMarker; } public void HandleDoors(Func connectionChecker) { var doors = GenerateDoors(); foreach (DoorMarker marker in doors) { var baseDir = marker.GetWorldDirection(); // WallIndex determines the offset *along* the edge of the room Vector2I offset = marker.Direction switch { DoorDirections.North => new Vector2I(marker.WallIndex, 0), DoorDirections.South => new Vector2I(marker.WallIndex, RoomResource.Size.Y - 1), DoorDirections.East => new Vector2I(RoomResource.Size.X - 1, marker.WallIndex), DoorDirections.West => new Vector2I(0, marker.WallIndex), _ => Vector2I.Zero }; // Combine GridPosition + offset to locate where this door aligns Vector2I doorEdge = GridPosition + offset; Vector2I neighborPos = doorEdge + baseDir; var connection = connectionChecker.Invoke(doorEdge, neighborPos); var connected = connection is not null; if (connected) { var door = this.CreateChildOf(marker, marker.Direction switch { DoorDirections.North => MapTheme.HorizontalDoorPrefab, DoorDirections.South => MapTheme.HorizontalDoorPrefab, DoorDirections.East => MapTheme.VerticalDoorPrefab, DoorDirections.West => MapTheme.VerticalDoorPrefab, _ => throw new ArgumentOutOfRangeException() }, marker.GlobalPosition); door.Close(); // door.State = DoorState.Closed; _doors.Add(door); if (doorEdge == connection.From) { connection.FromDoor = door; // Spawn lock if locked if (connection.IsLocked) { var doorLock = door.CreateChild(MapTheme.DoorLockPrefab); doorLock.Connection = connection; doorLock.ZIndex += 2; //doorLock.Targets.Add(door); //doorLock.Targets.Add(connection.ToDoor); doorLock.ActivationType = ActivationType.Open; } } else if (doorEdge == connection.To) { connection.ToDoor = door; } else { GD.Print( $"Door {door} connection was full: {connection.From} {connection.FromDoor} to {connection.To} {connection.ToDoor}"); } _connections.Add(connection); } else { // Move marker based on direction var newMarkerPosition = marker.Direction switch { DoorDirections.East => marker.GlobalPosition + new Vector2(-4, 0), DoorDirections.West => marker.GlobalPosition + new Vector2(-12, 0), DoorDirections.North => marker.GlobalPosition + new Vector2(0, 0), DoorDirections.South => marker.GlobalPosition + new Vector2(0, -2), _ => marker.GlobalPosition }; marker.GlobalPosition = newMarkerPosition; var wall = this.CreateChildOf(marker, marker.Direction switch { DoorDirections.North => MapTheme.HorizontalNorthWallPrefab, DoorDirections.South => MapTheme.HorizontalSouthWallPrefab, DoorDirections.East => MapTheme.VerticalWallPrefab, DoorDirections.West => MapTheme.VerticalWallPrefab, _ => throw new ArgumentOutOfRangeException() }, marker.GlobalPosition); } } } private void SpawnEnemies() { if (SpawnableEnemies is null || !SpawnableEnemies.Any()) return; var enemySpawnerMarkers = this.GetNode("EnemySpawners").GetChildren(); foreach (var marker in enemySpawnerMarkers) { if (marker is not RogueliteEnemySpawner spawner) continue; var spawnedEnemy = spawner.Spawn(MapTheme); _enemies.Add(spawnedEnemy); spawnedEnemy.Death += SpawnedEnemyOnDeath; } } private void SpawnFixedWeapons() { var markersContainer = this.GetNodeOrNull("Treasures"); if (markersContainer == null) return; var markerNodes = markersContainer.GetChildren(); var itemsPool = MapTheme.WeaponsLootTable.Items.ToList().Shuffle().ToList(); var playerItems = InventoryManager.Instance.Items; foreach (var itemContainer in playerItems) { if (itemsPool.Contains(itemContainer.Item)) { itemsPool.Remove(itemContainer.Item); } } if (itemsPool.Count == 0) { GD.Print("No items to spawn in the item room found"); return; } Queue spawnQueue = new(); foreach (var item in itemsPool) { spawnQueue.Enqueue(item); } foreach (var markerNode in markerNodes) { if (markerNode is not Marker2D marker) { continue; } var itemFound = spawnQueue.TryDequeue(out var item); if (!itemFound) return; GD.Print($"Spawning {item.ItemKey} in treasure spot"); var dropInstance = item.Spawn(marker); // Spawn // var dropScene = GD.Load(item.DropScenePath); // var dropInstance = marker.CreateChild(dropScene); } } private void SpawnFeatures() { // Get feature markers // Roll for chances // Spawn the objects var markersContainer = this.GetNodeOrNull("Features"); if (markersContainer is null) return; var markerNodes = markersContainer.GetChildren(); foreach (var markerNode in markerNodes) { if (markerNode is not Marker2D marker) { continue; } if (markerNode is ChestMarker chestMarker) { double roll = GD.RandRange(0d, 100d); double chance = chestMarker.OverrideChance ? chestMarker.SpawnChance : MapTheme.ChestChance; if (roll <= chance) { var hasLoot = MapTheme.ChestLootQueue.TryDequeue(out var loot); if (!hasLoot) { GD.Print("Ran out of loot to spawn"); continue; } var chest = marker.CreateChild(MapTheme.ChestPrefab); chest.LootTable.Add(loot); } } else if (markerNode is TeleporterMarker teleporterMarker) { var tp = teleporterMarker.Spawn(MapTheme); MapTheme.TeleportersList.Add(tp); Teleporters.Add(tp); } } } private void SpawnedEnemyOnDeath(EnemyFSMProxy enemy) { enemy.Death -= SpawnedEnemyOnDeath; _enemies.Remove(enemy); if (_enemies.Count == 0) { OpenDoors(); EnableLevelExitTeleporter(); EmitSignalRoomCleared(); } } private void EnableLevelExitTeleporter() { var teleporter = Teleporters.FirstOrDefault(x => x.Type is TeleporterMarkerType.NextLevel); if (teleporter is null) return; teleporter.SpawnedTeleporter.IsEnabled = true; teleporter.SpawnedTeleporter.Invisible = false; teleporter.SpawnedTeleporter.Show(); } public void OpenDoors() { foreach (var connection in _connections) { if (!connection.IsLocked) { connection.FromDoor?.Open(); } if (!connection.IsLocked) { connection.ToDoor?.Open(); } } } public void CloseDoors() { foreach (var connection in _connections) { if (!connection.IsLocked) { connection.FromDoor?.Close(); } if (!connection.IsLocked) { connection.ToDoor?.Close(); } } } private void OnRoomEntered(Area2D area) { if (area is not InteractionController player) return; if (_firstEnter) { SpawnEnemies(); _firstEnter = false; _shroud?.Activate(ActivationType.Disable); _ = AlarmTriggerAsync(area.GlobalPosition, 1f); } // This might cause problems later if I delay enemy spawns if (_enemies.Count <= 0) { OpenDoors(); // TODO: Just for testing EnableLevelExitTeleporter(); } else { CloseDoors(); } } private async Task AlarmTriggerAsync(Vector2 position, float secondsDelay) { await Task.Delay((int)(secondsDelay * 1000)); AlarmManager.Instance.SoundSilentAlarm(position); } private void OnRoomExited(Area2D area) { if (area is not InteractionController player) return; } public override string ToString() { return $"{GridPosition} {RoomResource}"; } public Vector2 GetDoorPosition(DoorDirections direction, int wallIndex) { return direction switch { DoorDirections.North => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * wallIndex), 32), DoorDirections.South => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * wallIndex), ((BaseRoomSize.Y) * RoomResource.Size.Y) + 2), DoorDirections.East => new Vector2((BaseRoomSize.X * RoomResource.Size.X) - 12, (BaseRoomSize.Y / 2) + (BaseRoomSize.Y * wallIndex) - 8), DoorDirections.West => new Vector2(12, (BaseRoomSize.Y / 2) + (BaseRoomSize.Y * wallIndex) - 8), _ => Vector2.Zero }; } // [ExportToolButton("Arrange Doors")] public Callable ArrangeDoorsButton => Callable.From(ArrangeDoors); public void ArrangeDoors() { var doorNode = this.GetNode("Doors"); var doors = doorNode.GetChildren(); foreach (var node in doors) { if (node is DoorMarker doorMarker) { GD.Print($"{doorMarker.Name} {doorMarker.Direction} {doorMarker.WallIndex}"); //var baseGridSize = new Vector2(320, 160); Vector2 doorPosition = doorMarker.Direction switch { DoorDirections.North => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * doorMarker.WallIndex), 32), DoorDirections.South => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * doorMarker.WallIndex), ((BaseRoomSize.Y) * RoomResource.Size.Y) + 2), DoorDirections.East => doorMarker.Position, DoorDirections.West => doorMarker.Position, _ => doorMarker.Position }; doorMarker.Position = doorPosition; } else { GD.Print($"Node was something else: {node}"); } } } public void SpawnShroud() { if (!RoomResource.StartShrouded) return; var shroud = this.CreateChild(MapTheme.ShroudPrefab); shroud.Position = new Vector2(RoomSize.X / 2, RoomSize.Y / 2); shroud.Scale = RoomSize; _shroud = shroud; } }