summaryrefslogtreecommitdiff
path: root/scripts/Map/DungeonGenerator.cs
diff options
context:
space:
mode:
authorMatheus <matheus.guedes.mg.m@gmail.com>2025-09-09 19:09:34 -0300
committerMatheus <matheus.guedes.mg.m@gmail.com>2025-09-09 19:09:34 -0300
commitc6bbb834f7758027c0df338f1520f34fad3befea (patch)
tree1818cd23c24be16fbe19b16dd0a510874d440d83 /scripts/Map/DungeonGenerator.cs
parentf1b51bed52ffbd90b5b7cc8dcfc6f0484bbbeb3c (diff)
Organização
Diffstat (limited to 'scripts/Map/DungeonGenerator.cs')
-rw-r--r--scripts/Map/DungeonGenerator.cs301
1 files changed, 301 insertions, 0 deletions
diff --git a/scripts/Map/DungeonGenerator.cs b/scripts/Map/DungeonGenerator.cs
new file mode 100644
index 0000000..9a44d33
--- /dev/null
+++ b/scripts/Map/DungeonGenerator.cs
@@ -0,0 +1,301 @@
+using Godot;
+using TheLegendOfGustav.Entities.Actors;
+using TheLegendOfGustav.Entities;
+using TheLegendOfGustav.Entities.Items;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// A classe dungeonGenerator cria exatamente um andar da masmorra.
+/// Ela é chamada quando necessário.
+/// </summary>
+public partial class DungeonGenerator : Node
+{
+ #region Fields
+ /// <summary>
+ /// Coleção de todos os inimigos que o gerador tem acesso.
+ /// </summary>
+ private static readonly Godot.Collections.Array<EnemyDefinition> enemies = [
+ GD.Load<EnemyDefinition>("res://assets/definitions/actor/Skeleton.tres"),
+ GD.Load<EnemyDefinition>("res://assets/definitions/actor/morcegao.tres"),
+ GD.Load<EnemyDefinition>("res://assets/definitions/actor/Shadow.tres"),
+ ];
+
+ private static readonly Godot.Collections.Array<ConsumableItemDefinition> items = [
+ GD.Load<HealingConsumableDefinition>("res://assets/definitions/Items/small_healing_potion.tres")
+ ];
+ #endregion
+
+ #region Properties
+ /// <summary>
+ /// Dimensões do mapa a ser criado.
+ /// </summary>
+ [ExportCategory("Dimension")]
+ [Export]
+ private int Width { get; set; } = 80;
+ [Export]
+ private int Height { get; set; } = 60;
+
+ /// <summary>
+ /// Gerador de números aleatórios
+ /// </summary>
+ [ExportCategory("RNG")]
+ private RandomNumberGenerator Rng { get; set; } = new();
+ /// <summary>
+ /// Qual seed utilizar.
+ /// </summary>
+ [Export]
+ private ulong Seed { get; set; }
+ /// <summary>
+ /// Se será utilizada a nossa seed ou a seed padrão da classe RandomNumberGenerator.
+ /// </summary>
+ [Export]
+ private bool UseSeed { get; set; } = true;
+ /// <summary>
+ /// Quantas iterações do algoritmo chamar.
+ /// </summary>
+ [Export]
+ private int Iterations { get; set; } = 3;
+
+ /// <summary>
+ /// Quantidade máxima de inimigos por sala.
+ /// </summary>
+ [ExportCategory("Monster RNG")]
+ [Export]
+ private int MaxMonsterPerRoom { get; set; } = 2;
+
+ /// <summary>
+ /// Quantidade máxima de itens por sala.
+ /// </summary>
+ [ExportCategory("Loot RNG")]
+ [Export]
+ private int MaxItemsPerRoom { get; set; } = 2;
+ #endregion
+
+ #region Methods
+ public override void _Ready()
+ {
+ base._Ready();
+ if (UseSeed)
+ {
+ Rng.Seed = Seed;
+ }
+ }
+
+ /// <summary>
+ /// Gera um andar da masmorra.
+ /// Inimigos são colocados conforme configurações.
+ /// O jogador é colocado na primeira sala gerada.
+ /// </summary>
+ /// <param name="player">Jogador.</param>
+ /// <returns>O mapa gerado.</returns>
+ public MapData GenerateDungeon(Player player)
+ {
+ MapData data = new MapData(Width, Height, player);
+
+ // Divisão mestre que engloba o mapa inteiro.
+ MapDivision root = new MapDivision(0, 0, Width, Height);
+
+ // Chama o algoritmo para dividir o mapa.
+ root.Split(Iterations, Rng);
+
+ bool first = true;
+
+ // Coloca os corredores.
+ TunnelDivisions(data, root);
+
+ // Cria as salas com base nas divisões geradas.
+ foreach (MapDivision division in root.GetLeaves())
+ {
+ Rect2I room = new(division.Position, division.Size);
+
+ // A sala não pode oculpar a divisão inteira, senão não haveriam paredes.
+ room = room.GrowIndividual(
+ -Rng.RandiRange(1, 2),
+ -Rng.RandiRange(1, 2),
+ -Rng.RandiRange(1, 2),
+ -Rng.RandiRange(1, 2)
+ );
+
+ // De fato cria a sala.
+ CarveRoom(data, room);
+ // Colocamos o jogador na primeira sala.
+ if (first)
+ {
+ first = false;
+ player.GridPosition = room.GetCenter();
+ }
+ // Colocamos os inimigos na sala.
+ PlaceEntities(data, room);
+ }
+
+ // Feito o mapa, inicializamos o algoritmo de pathfinding.
+ data.SetupPathfinding();
+ return data;
+ }
+
+ /// <summary>
+ /// Transforma o tile da posição especificada em chão.
+ /// </summary>
+ /// <param name="data">o mapa</param>
+ /// <param name="pos">posição para colocar o chão.</param>
+ private static void CarveTile(MapData data, Vector2I pos)
+ {
+ Tile tile = data.GetTile(pos);
+ if (tile == null) return;
+
+ tile.SetDefinition(MapData.floorDefinition);
+ }
+
+ /// <summary>
+ /// Preenche uma área retangular com chão.
+ /// </summary>
+ /// <param name="data">O mapa</param>
+ /// <param name="room">Área para preencher com chão</param>
+ private static void CarveRoom(MapData data, Rect2I room)
+ {
+ for (int y = room.Position.Y; y < room.End.Y; y++)
+ {
+ for (int x = room.Position.X; x < room.End.X; x++)
+ {
+ CarveTile(data, new Vector2I(x, y));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Preenche uma linha horizontal com chão.
+ /// </summary>
+ /// <param name="data">O mapa</param>
+ /// <param name="y">Eixo y do corredor.</param>
+ /// <param name="xBegin">Início do corredor</param>
+ /// <param name="xEnd">Final do corredor.</param>
+ private static void HorizontalCorridor(MapData data, int y, int xBegin, int xEnd)
+ {
+ int begin = (xBegin < xEnd) ? xBegin : xEnd;
+ int end = (xEnd > xBegin) ? xEnd : xBegin;
+ for (int i = begin; i <= end; i++)
+ {
+ CarveTile(data, new Vector2I(i, y));
+ }
+ }
+
+ /// <summary>
+ /// Cria recursivamente corredores entre o centro de cada divisão do mapa.
+ /// </summary>
+ /// <param name="data">O mapa</param>
+ /// <param name="root">Divisão mestre.</param>
+ private static void TunnelDivisions(MapData data, MapDivision root)
+ {
+ if (root.IsLeaf)
+ {
+ return;
+ }
+
+ TunnelBetween(data, root.Right.Center, root.Left.Center);
+ TunnelDivisions(data, root.Left);
+ TunnelDivisions(data, root.Right);
+ }
+
+ /// <summary>
+ /// Preenche uma linha vertical com chão.
+ /// </summary>
+ /// <param name="data">O mapa.</param>
+ /// <param name="x">Eixo x do corredor.</param>
+ /// <param name="yBegin">Início do corredor</param>
+ /// <param name="yEnd">Final do corredor.</param>
+ private static void VerticalCorridor(MapData data, int x, int yBegin, int yEnd)
+ {
+ int begin = (yBegin < yEnd) ? yBegin : yEnd;
+ int end = (yEnd > yBegin) ? yEnd : yBegin;
+ for (int i = begin; i <= end; i++)
+ {
+ CarveTile(data, new Vector2I(x, i));
+ }
+ }
+
+ /// <summary>
+ /// Cria corredores vertical e horizontal para unir dois pontos no mapa.
+ /// </summary>
+ /// <param name="data">O mapa</param>
+ /// <param name="start">Ponto inicial</param>
+ /// <param name="end">Ponto final.</param>
+ private static void TunnelBetween(MapData data, Vector2I start, Vector2I end)
+ {
+ HorizontalCorridor(data, start.Y, start.X, end.X);
+ VerticalCorridor(data, end.X, start.Y, end.Y);
+ }
+
+ /// <summary>
+ /// Popula uma sala com inimigos.
+ /// </summary>
+ /// <param name="data">O mapa</param>
+ /// <param name="room">A sala.</param>
+ private void PlaceEntities(MapData data, Rect2I room)
+ {
+ // Define quantos monstros serão colocados na sala
+ int monsterAmount = Rng.RandiRange(0, MaxMonsterPerRoom);
+ // Define quantos itens serão colocados na sala.
+ int itemAmount = Rng.RandiRange(0, MaxItemsPerRoom);
+
+ for (int i = 0; i < monsterAmount; i++)
+ {
+ // Escolhe um lugar aleatório na sala.
+ Vector2I position = new(
+ Rng.RandiRange(room.Position.X, room.End.X - 1),
+ Rng.RandiRange(room.Position.Y, room.End.Y - 1)
+ );
+
+ // Só podemos colocar um ator por ponto no espaço.
+ bool canPlace = true;
+ foreach (Entity entity in data.Entities)
+ {
+ if (entity.GridPosition == position)
+ {
+ canPlace = false;
+ break;
+ }
+ }
+
+ // Se possível, criamos um inimigo aleatório na posição escolhida.
+ if (canPlace)
+ {
+ EnemyDefinition definition = enemies.PickRandom();
+ Enemy enemy = new(position, data, definition);
+ data.InsertEntity(enemy);
+ }
+ }
+
+ for (int i = 0; i < itemAmount; i++)
+ {
+ // Escolhe um lugar aleatório na sala.
+ Vector2I position = new(
+ Rng.RandiRange(room.Position.X, room.End.X - 1),
+ Rng.RandiRange(room.Position.Y, room.End.Y - 1)
+ );
+
+ // Só podemos colocar um ator por ponto no espaço.
+ bool canPlace = true;
+ foreach (Entity entity in data.Entities)
+ {
+ if (entity.GridPosition == position)
+ {
+ canPlace = false;
+ break;
+ }
+ }
+
+ // Se possível, criamos um inimigo aleatório na posição escolhida.
+ if (canPlace)
+ {
+ ConsumableItemDefinition definition = items.PickRandom();
+ if (definition is HealingConsumableDefinition hcDefinition)
+ {
+ HealingConsumable item = new(position, data, hcDefinition);
+ data.InsertEntity(item);
+ }
+ }
+ }
+ }
+ #endregion
+}