cirnogodot/Scripts/Controllers/RogueliteRoom.cs

524 lines
16 KiB
C#
Raw Permalink Normal View History

2025-04-11 18:39:39 +02:00
using System;
2025-04-16 18:18:52 +02:00
using System.Collections.Generic;
2025-04-11 18:39:39 +02:00
using System.Linq;
2025-04-28 12:22:00 +02:00
using System.Threading.Tasks;
2025-04-28 09:50:55 +02:00
using Cirno.Scripts.Activables;
2025-04-28 12:22:00 +02:00
using Cirno.Scripts.Actors;
2025-04-16 18:18:52 +02:00
using Cirno.Scripts.Components.FSM.Enemy;
2025-04-22 18:21:53 +02:00
using Cirno.Scripts.Enums;
2025-04-24 16:40:51 +02:00
using Cirno.Scripts.Interactables;
2025-04-11 15:53:59 +02:00
using Cirno.Scripts.Resources;
2025-04-25 18:33:20 +02:00
using Cirno.Scripts.Resources.Loot;
2025-04-11 15:53:59 +02:00
using Cirno.Scripts.Resources.Roguelite;
2025-04-25 18:33:20 +02:00
using Cirno.Scripts.Utils;
2025-04-10 19:04:06 +02:00
using Godot;
2025-04-11 15:53:59 +02:00
using Godot.Collections;
2025-04-29 12:12:47 +02:00
using Array = Godot.Collections.Array;
2025-04-10 19:04:06 +02:00
namespace Cirno.Scripts.Controllers;
2025-04-23 13:38:54 +02:00
[Tool]
2025-04-10 19:04:06 +02:00
public partial class RogueliteRoom : Node2D
{
[Export] public RogueliteRoomResource RoomResource { get; set; }
2025-04-30 15:09:59 +02:00
[Export] public Array<Node2D> RoomClearActivation { get; set; }
2025-04-28 14:58:41 +02:00
2025-04-24 16:40:51 +02:00
public RogueliteMapTheme MapTheme { get; set; }
2025-04-14 16:50:58 +02:00
2025-04-11 18:39:39 +02:00
public Vector2I GridPosition { get; set; } // Set by dungeon manager
2025-04-14 16:50:58 +02:00
2025-04-21 17:46:26 +02:00
public Vector2I BottomLeft => GridPosition + new Vector2I(0, RoomResource.Size.Y - 1);
2025-04-28 09:50:55 +02:00
//private Vector2 BaseRoomSize => new Vector2(320, 160);
private Vector2 BaseRoomSize => MapTheme.TileSize * MapTheme.RoomSizeInTiles;
2025-04-23 14:27:30 +02:00
2025-04-28 09:50:55 +02:00
public Vector2 RoomSize => BaseRoomSize * RoomResource.Size;
2025-04-28 14:58:41 +02:00
2025-04-30 15:09:59 +02:00
[Signal]
public delegate void RoomClearedEventHandler();
2025-04-22 13:50:26 +02:00
public Vector2I RandomBottomExit()
{
return BottomLeft + new Vector2I(GD.RandRange(0, RoomResource.Size.X - 1), 0);
}
2025-04-23 14:27:30 +02:00
2025-04-22 18:21:53 +02:00
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)
};
2025-04-23 14:27:30 +02:00
}
2025-04-16 18:18:52 +02:00
private static readonly Godot.Collections.Dictionary<string, Vector2I> DirectionMap = new()
2025-04-11 18:39:39 +02:00
{
{ "North", new Vector2I(0, -1) },
{ "South", new Vector2I(0, 1) },
2025-04-14 16:50:58 +02:00
{ "East", new Vector2I(1, 0) },
{ "West", new Vector2I(-1, 0) },
2025-04-11 18:39:39 +02:00
};
2025-04-11 15:53:59 +02:00
2025-04-16 18:18:52 +02:00
private List<Door> _doors = [];
private List<RoomConnection> _connections = [];
private List<EnemyFSMProxy> _enemies = [];
2025-04-29 18:14:09 +02:00
public List<TeleporterMarker> Teleporters { get; private set; } = [];
2025-04-30 15:09:59 +02:00
2025-04-14 16:50:58 +02:00
private Array<EnemyResource> SpawnableEnemies => RoomResource.SpawnableEnemies;
2025-04-28 14:58:41 +02:00
2025-04-28 12:22:00 +02:00
private BlackCover _shroud;
private bool _firstEnter = true;
2025-04-11 15:53:59 +02:00
2025-04-14 16:50:58 +02:00
public RogueliteRoom Spawn()
2025-04-11 18:39:39 +02:00
{
2025-04-28 12:22:00 +02:00
//SpawnEnemies();
2025-04-25 18:33:20 +02:00
SpawnFeatures();
2025-04-25 19:00:28 +02:00
SpawnFixedWeapons();
2025-04-28 09:50:55 +02:00
SpawnShroud();
2025-04-14 16:50:58 +02:00
//HandleDoors(connectionChecker);
2025-04-25 18:33:20 +02:00
//AddDebugLabel();
2025-04-11 18:39:39 +02:00
return this;
}
2025-04-14 16:50:58 +02:00
2025-04-16 15:11:29 +02:00
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);
}
}
}
2025-04-23 13:38:54 +02:00
private List<DoorMarker> GenerateDoors()
2025-04-11 18:39:39 +02:00
{
2025-04-23 13:38:54 +02:00
List<DoorMarker> doors = [];
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
var doorsContainer = new Node2D();
this.AddChild(doorsContainer);
doorsContainer.Name = "Doors";
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
// North
for (int i = 0; i < RoomResource.Size.X; i++)
{
if (RoomResource.HasDoors(DoorDirections.North))
{
doors.Add(MakeDoorMarker(DoorDirections.North, i));
}
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
if (RoomResource.HasDoors(DoorDirections.South))
{
doors.Add(MakeDoorMarker(DoorDirections.South, i));
}
}
2025-04-11 18:39:39 +02:00
2025-04-23 13:38:54 +02:00
for (int j = 0; j < RoomResource.Size.Y; j++)
2025-04-11 18:39:39 +02:00
{
2025-04-23 13:38:54 +02:00
doors.Add(MakeDoorMarker(DoorDirections.East, j));
doors.Add(MakeDoorMarker(DoorDirections.West, j));
}
foreach (var door in doors)
{
doorsContainer.AddChild(door);
}
2025-04-14 16:50:58 +02:00
2025-04-23 13:38:54 +02:00
return doors;
}
private DoorMarker MakeDoorMarker(DoorDirections direction, int wallIndex)
{
var doorMarker = new DoorMarker();
doorMarker.Direction = direction;
doorMarker.WallIndex = wallIndex;
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
doorMarker.Position = GetDoorPosition(direction, wallIndex);
return doorMarker;
}
public void HandleDoors(Func<Vector2I, Vector2I, RoomConnection> connectionChecker)
{
var doors = GenerateDoors();
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
foreach (DoorMarker marker in doors)
{
2025-04-11 18:39:39 +02:00
var baseDir = marker.GetWorldDirection();
// WallIndex determines the offset *along* the edge of the room
Vector2I offset = marker.Direction switch
{
2025-04-23 13:38:54 +02:00
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),
2025-04-11 18:39:39 +02:00
_ => Vector2I.Zero
};
// Combine GridPosition + offset to locate where this door aligns
Vector2I doorEdge = GridPosition + offset;
Vector2I neighborPos = doorEdge + baseDir;
2025-04-16 15:11:29 +02:00
var connection = connectionChecker.Invoke(doorEdge, neighborPos);
var connected = connection is not null;
2025-04-14 16:50:58 +02:00
if (connected)
{
2025-04-28 14:58:41 +02:00
var door = this.CreateChildOf<Door>(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;
2025-04-16 18:18:52 +02:00
_doors.Add(door);
2025-04-17 16:51:20 +02:00
if (doorEdge == connection.From)
2025-04-17 13:04:56 +02:00
{
connection.FromDoor = door;
2025-04-28 14:58:41 +02:00
2025-04-24 16:40:51 +02:00
// Spawn lock if locked
if (connection.IsLocked)
{
var doorLock = door.CreateChild<RogueliteDoorLock>(MapTheme.DoorLockPrefab);
doorLock.Connection = connection;
2025-04-28 14:58:41 +02:00
2025-04-24 16:40:51 +02:00
doorLock.ZIndex += 2;
//doorLock.Targets.Add(door);
//doorLock.Targets.Add(connection.ToDoor);
doorLock.ActivationType = ActivationType.Open;
}
2025-04-17 13:04:56 +02:00
}
2025-04-17 16:51:20 +02:00
else if (doorEdge == connection.To)
2025-04-17 13:04:56 +02:00
{
connection.ToDoor = door;
}
else
{
2025-04-23 14:27:30 +02:00
GD.Print(
$"Door {door} connection was full: {connection.From} {connection.FromDoor} to {connection.To} {connection.ToDoor}");
2025-04-17 13:04:56 +02:00
}
2025-04-16 18:18:52 +02:00
2025-04-23 14:27:30 +02:00
_connections.Add(connection);
2025-04-14 16:50:58 +02:00
}
else
{
2025-04-28 14:58:41 +02:00
// Move marker based on direction
var newMarkerPosition = marker.Direction switch
{
2025-04-30 15:09:59 +02:00
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),
2025-04-28 14:58:41 +02:00
_ => marker.GlobalPosition
};
marker.GlobalPosition = newMarkerPosition;
2025-04-30 15:09:59 +02:00
2025-04-28 14:58:41 +02:00
var wall = this.CreateChildOf<Node2D>(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);
2025-04-14 16:50:58 +02:00
}
2025-04-11 18:39:39 +02:00
}
}
private void SpawnEnemies()
2025-04-11 15:53:59 +02:00
{
2025-04-11 18:39:39 +02:00
if (SpawnableEnemies is null || !SpawnableEnemies.Any()) return;
2025-04-14 16:50:58 +02:00
2025-04-28 12:22:00 +02:00
var enemySpawnerMarkers = this.GetNode("EnemySpawners").GetChildren();
2025-04-11 15:53:59 +02:00
2025-04-28 12:22:00 +02:00
foreach (var marker in enemySpawnerMarkers)
2025-04-11 15:53:59 +02:00
{
2025-04-28 12:22:00 +02:00
if (marker is not RogueliteEnemySpawner spawner) continue;
2025-04-14 16:50:58 +02:00
2025-04-28 12:22:00 +02:00
var spawnedEnemy = spawner.Spawn(MapTheme);
2025-04-28 14:58:41 +02:00
2025-04-16 18:18:52 +02:00
_enemies.Add(spawnedEnemy);
2025-04-23 14:27:30 +02:00
2025-04-16 18:18:52 +02:00
spawnedEnemy.Death += SpawnedEnemyOnDeath;
}
}
2025-04-25 19:00:28 +02:00
private void SpawnFixedWeapons()
{
var markersContainer = this.GetNodeOrNull("Treasures");
if (markersContainer == null) return;
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
var markerNodes = markersContainer.GetChildren();
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
var itemsPool = MapTheme.WeaponsLootTable.Items.ToList().Shuffle().ToList();
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
var playerItems = InventoryManager.Instance.Items;
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
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;
}
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
Queue<LootItem> spawnQueue = new();
foreach (var item in itemsPool)
{
spawnQueue.Enqueue(item);
}
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
foreach (var markerNode in markerNodes)
{
if (markerNode is not Marker2D marker)
{
continue;
}
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
var itemFound = spawnQueue.TryDequeue(out var item);
2025-04-28 14:58:41 +02:00
2025-04-25 19:00:28 +02:00
if (!itemFound) return;
GD.Print($"Spawning {item.ItemKey} in treasure spot");
2025-04-28 14:58:41 +02:00
var dropInstance = item.Spawn(marker);
2025-04-25 19:00:28 +02:00
// Spawn
// var dropScene = GD.Load<PackedScene>(item.DropScenePath);
// var dropInstance = marker.CreateChild<Node2D>(dropScene);
2025-04-25 19:00:28 +02:00
}
}
2025-04-25 18:33:20 +02:00
private void SpawnFeatures()
{
// Get feature markers
// Roll for chances
// Spawn the objects
2025-04-28 14:58:41 +02:00
2025-04-25 18:33:20 +02:00
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;
}
2025-04-28 14:58:41 +02:00
2025-04-29 12:12:47 +02:00
if (markerNode is ChestMarker chestMarker)
2025-04-25 18:33:20 +02:00
{
2025-04-29 12:12:47 +02:00
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");
2025-04-29 18:14:09 +02:00
continue;
2025-04-29 12:12:47 +02:00
}
2025-04-30 15:09:59 +02:00
2025-04-29 12:12:47 +02:00
var chest = marker.CreateChild<Chest>(MapTheme.ChestPrefab);
2025-04-30 15:09:59 +02:00
2025-04-29 12:12:47 +02:00
chest.LootTable.Add(loot);
}
2025-04-25 18:33:20 +02:00
}
2025-04-29 18:14:09 +02:00
else if (markerNode is TeleporterMarker teleporterMarker)
{
var tp = teleporterMarker.Spawn(MapTheme);
2025-04-30 15:09:59 +02:00
2025-04-29 18:14:09 +02:00
MapTheme.TeleportersList.Add(tp);
Teleporters.Add(tp);
}
2025-04-25 18:33:20 +02:00
}
}
2025-04-16 18:18:52 +02:00
private void SpawnedEnemyOnDeath(EnemyFSMProxy enemy)
{
enemy.Death -= SpawnedEnemyOnDeath;
_enemies.Remove(enemy);
if (_enemies.Count == 0)
{
OpenDoors();
2025-04-30 15:09:59 +02:00
EnableLevelExitTeleporter();
2025-04-29 12:12:47 +02:00
EmitSignalRoomCleared();
2025-04-16 18:18:52 +02:00
}
}
2025-04-30 15:09:59 +02:00
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;
2025-05-01 14:44:48 +02:00
teleporter.SpawnedTeleporter.Show();
2025-04-30 15:09:59 +02:00
}
2025-04-16 18:18:52 +02:00
public void OpenDoors()
{
foreach (var connection in _connections)
{
2025-04-28 14:58:41 +02:00
if (!connection.IsLocked)
2025-04-24 16:40:51 +02:00
{
connection.FromDoor?.Open();
}
2025-04-28 14:58:41 +02:00
if (!connection.IsLocked)
2025-04-24 16:40:51 +02:00
{
connection.ToDoor?.Open();
}
2025-04-11 15:53:59 +02:00
}
}
2025-04-16 18:18:52 +02:00
public void CloseDoors()
{
foreach (var connection in _connections)
{
2025-04-28 14:58:41 +02:00
if (!connection.IsLocked)
2025-04-24 16:40:51 +02:00
{
connection.FromDoor?.Close();
}
2025-04-28 14:58:41 +02:00
if (!connection.IsLocked)
2025-04-24 16:40:51 +02:00
{
connection.ToDoor?.Close();
}
2025-04-16 18:18:52 +02:00
}
}
private void OnRoomEntered(Area2D area)
{
if (area is not InteractionController player) return;
2025-04-23 14:27:30 +02:00
2025-04-28 12:22:00 +02:00
if (_firstEnter)
{
SpawnEnemies();
_firstEnter = false;
_shroud?.Activate(ActivationType.Disable);
_ = AlarmTriggerAsync(area.GlobalPosition, 1f);
}
2025-04-28 14:58:41 +02:00
2025-04-28 12:22:00 +02:00
// This might cause problems later if I delay enemy spawns
2025-04-16 18:18:52 +02:00
if (_enemies.Count <= 0)
{
OpenDoors();
2025-04-30 15:09:59 +02:00
// TODO: Just for testing
EnableLevelExitTeleporter();
2025-04-16 18:18:52 +02:00
}
else
{
CloseDoors();
}
}
2025-04-23 14:27:30 +02:00
2025-04-28 12:22:00 +02:00
private async Task AlarmTriggerAsync(Vector2 position, float secondsDelay)
{
await Task.Delay((int)(secondsDelay * 1000));
AlarmManager.Instance.SoundSilentAlarm(position);
}
2025-04-16 18:18:52 +02:00
private void OnRoomExited(Area2D area)
{
if (area is not InteractionController player) return;
}
2025-04-23 14:27:30 +02:00
2025-04-21 17:46:26 +02:00
public override string ToString()
{
return $"{GridPosition} {RoomResource}";
}
2025-04-23 13:38:54 +02:00
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),
2025-04-23 14:27:30 +02:00
((BaseRoomSize.Y) * RoomResource.Size.Y) + 2),
DoorDirections.East => new Vector2((BaseRoomSize.X * RoomResource.Size.X) - 12,
(BaseRoomSize.Y / 2) + (BaseRoomSize.Y * wallIndex) - 8),
2025-04-23 13:38:54 +02:00
DoorDirections.West => new Vector2(12, (BaseRoomSize.Y / 2) + (BaseRoomSize.Y * wallIndex) - 8),
_ => Vector2.Zero
};
}
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
// [ExportToolButton("Arrange Doors")] public Callable ArrangeDoorsButton => Callable.From(ArrangeDoors);
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
public void ArrangeDoors()
{
var doorNode = this.GetNode("Doors");
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
var doors = doorNode.GetChildren();
2025-04-23 14:27:30 +02:00
2025-04-23 13:38:54 +02:00
foreach (var node in doors)
{
if (node is DoorMarker doorMarker)
{
GD.Print($"{doorMarker.Name} {doorMarker.Direction} {doorMarker.WallIndex}");
//var baseGridSize = new Vector2(320, 160);
2025-04-23 13:38:54 +02:00
Vector2 doorPosition = doorMarker.Direction switch
{
DoorDirections.North => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * doorMarker.WallIndex),
2025-04-23 14:27:30 +02:00
32),
DoorDirections.South => new Vector2((BaseRoomSize.X / 2) + (BaseRoomSize.X * doorMarker.WallIndex),
((BaseRoomSize.Y) * RoomResource.Size.Y) + 2),
2025-04-23 13:38:54 +02:00
DoorDirections.East => doorMarker.Position,
DoorDirections.West => doorMarker.Position,
_ => doorMarker.Position
};
doorMarker.Position = doorPosition;
}
else
{
GD.Print($"Node was something else: {node}");
}
}
}
2025-04-28 09:50:55 +02:00
public void SpawnShroud()
{
if (!RoomResource.StartShrouded) return;
var shroud = this.CreateChild<BlackCover>(MapTheme.ShroudPrefab);
2025-04-28 14:58:41 +02:00
2025-04-28 09:50:55 +02:00
shroud.Position = new Vector2(RoomSize.X / 2, RoomSize.Y / 2);
shroud.Scale = RoomSize;
2025-04-28 12:22:00 +02:00
_shroud = shroud;
2025-04-28 09:50:55 +02:00
}
2025-04-10 19:04:06 +02:00
}