using System; using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Activables; 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; namespace Cirno.Scripts.Controllers; [Tool] public partial class RogueliteRoom : Node2D { [Export] public RogueliteRoomResource RoomResource { 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; 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 = []; private Array SpawnableEnemies => RoomResource.SpawnableEnemies; 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; 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; if (connected) { _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 { // var wall = this.CreateChild(WallPrefab, marker.GlobalPosition); } } } private void SpawnEnemies() { if (SpawnableEnemies is null || !SpawnableEnemies.Any()) return; var enemySpawners = this.GetNode("EnemySpawners").GetChildren().Cast(); foreach (var spawner in enemySpawners) { var index = GD.RandRange(0, SpawnableEnemies.Count - 1); var e = SpawnableEnemies[index]; var enemyScene = GD.Load(e.PrefabPath); var spawnedEnemy = spawner.CreateChild(enemyScene); spawnedEnemy.OverrideLoot = true; spawnedEnemy.ExtraLoot.Add(new LootDrop() { Item = MapTheme.EnemiesLootTable.Items.ToList().Shuffle().First(), Chance = MapTheme.EnemyDropChance }); spawnedEnemy.ExtraLoot.Add(new LootDrop() { Item = MapTheme.PointItemResource, Chance = 100 }); _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"); // 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; } double chance = GD.RandRange(0d, 100d); if (chance <= MapTheme.ChestChance) { var chest = marker.CreateChild(MapTheme.ChestPrefab); var loot = MapTheme.ChestLootTable.Items.ToList().Shuffle().First(); chest.LootTable.Add(loot); } } } private void SpawnedEnemyOnDeath(EnemyFSMProxy enemy) { enemy.Death -= SpawnedEnemyOnDeath; _enemies.Remove(enemy); if (_enemies.Count == 0) { OpenDoors(); } } 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 (_enemies.Count <= 0) { OpenDoors(); } else { CloseDoors(); } } 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((baseGridSize.X / 2) + (baseGridSize.X * doorMarker.WallIndex), 32), DoorDirections.South => new Vector2((baseGridSize.X / 2) + (baseGridSize.X * doorMarker.WallIndex), ((baseGridSize.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; } }