summaryrefslogtreecommitdiff
path: root/scripts/Map
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/Map')
-rw-r--r--scripts/Map/DungeonGenerator.cs301
-rw-r--r--scripts/Map/DungeonGenerator.cs.uid1
-rw-r--r--scripts/Map/FieldOfView.cs115
-rw-r--r--scripts/Map/FieldOfView.cs.uid1
-rw-r--r--scripts/Map/Map.cs98
-rw-r--r--scripts/Map/Map.cs.uid1
-rw-r--r--scripts/Map/MapData.cs318
-rw-r--r--scripts/Map/MapData.cs.uid1
-rw-r--r--scripts/Map/MapDivision.cs113
-rw-r--r--scripts/Map/MapDivision.cs.uid1
-rw-r--r--scripts/Map/Tile.cs90
-rw-r--r--scripts/Map/Tile.cs.uid1
-rw-r--r--scripts/Map/TileDefinition.cs24
-rw-r--r--scripts/Map/TileDefinition.cs.uid1
14 files changed, 1066 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
+}
diff --git a/scripts/Map/DungeonGenerator.cs.uid b/scripts/Map/DungeonGenerator.cs.uid
new file mode 100644
index 0000000..15aeef6
--- /dev/null
+++ b/scripts/Map/DungeonGenerator.cs.uid
@@ -0,0 +1 @@
+uid://dwyr067lwqcsj
diff --git a/scripts/Map/FieldOfView.cs b/scripts/Map/FieldOfView.cs
new file mode 100644
index 0000000..40df320
--- /dev/null
+++ b/scripts/Map/FieldOfView.cs
@@ -0,0 +1,115 @@
+using Godot;
+
+namespace TheLegendOfGustav.Map;
+
+// Copiado e adaptado deste cara aqui: https://www.roguebasin.com/index.php?title=C%2B%2B_shadowcasting_implementation e deste também https://selinadev.github.io/08-rogueliketutorial-04/
+
+// Eu não vou mentir, li como o algoritmo funciona, mas mesmo assim não entendi.
+
+public partial class FieldOfView : Node
+{
+
+ private Godot.Collections.Array<Tile> fov = [];
+
+ private static readonly int[,] multipliers = new int[4, 8]{
+ {1, 0, 0, -1, -1, 0, 0, 1},
+ {0, 1, -1, 0, 0, -1, 1, 0},
+ {0, 1, 1, 0, 0, -1, -1, 0},
+ {1, 0, 0, 1, -1, 0, 0, -1}
+ };
+ private void CastLight(MapData data, Vector2I pos, int radius, int row, float startSlope, float endSlope, int xx, int xy, int yx, int yy)
+ {
+ if (startSlope < endSlope)
+ {
+ return;
+ }
+
+ float nextStartSlope = startSlope;
+ for (int i = row; i <= radius; i++)
+ {
+ bool blocked = false;
+ for (int dx = -i, dy = -i; dx <= 0; dx++)
+ {
+ float lSlope = (float)((dx - 0.5) / (dy + 0.5));
+ float rSlope = (float)((dx + 0.5) / (dy - 0.5));
+
+ if (startSlope < rSlope)
+ {
+ continue;
+ }
+ else if (endSlope > lSlope)
+ {
+ break;
+ }
+
+ int sax = dx * xx + dy * xy;
+ int say = dx * yx + dy * yy;
+
+ if ((sax < 0 && int.Abs(sax) > pos.X) || (say < 0 && int.Abs(say) > pos.Y))
+ {
+ continue;
+ }
+ int ax = pos.X + sax;
+ int ay = pos.Y + say;
+
+ if (ax >= data.Width || ay >= data.Height)
+ {
+ continue;
+ }
+
+ Tile currentTile = data.GetTile(ax, ay);
+ int radius2 = radius * radius;
+ if ((dx * dx + dy * dy) < radius2)
+ {
+ currentTile.IsInView = true;
+ fov.Add(currentTile);
+ }
+
+ if (blocked)
+ {
+ if (!currentTile.IsTransparent)
+ {
+ nextStartSlope = rSlope;
+ continue;
+ }
+ else
+ {
+ blocked = false;
+ startSlope = nextStartSlope;
+ }
+ }
+ else if (!currentTile.IsTransparent)
+ {
+ blocked = true;
+ nextStartSlope = rSlope;
+ CastLight(data, pos, radius, i + 1, startSlope, lSlope, xx, xy, yx, yy);
+ }
+ }
+ if (blocked)
+ {
+ break;
+ }
+ }
+ }
+
+ private void ClearFOV()
+ {
+ foreach (Tile tile in fov)
+ {
+ tile.IsInView = false;
+ }
+ fov.Clear();
+ }
+
+ public void UpdateFOV(MapData data, Vector2I position, int radius)
+ {
+ ClearFOV();
+ Tile start = data.GetTile(position);
+ start.IsInView = true;
+ fov.Add(start);
+ for (int i = 0; i < 8; i++)
+ {
+ CastLight(data, position, radius, 1, 1.0f, 0.0f, multipliers[0, i], multipliers[1, i], multipliers[2, i], multipliers[3, i]);
+ }
+ }
+}
diff --git a/scripts/Map/FieldOfView.cs.uid b/scripts/Map/FieldOfView.cs.uid
new file mode 100644
index 0000000..a173ff3
--- /dev/null
+++ b/scripts/Map/FieldOfView.cs.uid
@@ -0,0 +1 @@
+uid://bereyrj1s46y5
diff --git a/scripts/Map/Map.cs b/scripts/Map/Map.cs
new file mode 100644
index 0000000..9117bc5
--- /dev/null
+++ b/scripts/Map/Map.cs
@@ -0,0 +1,98 @@
+using Godot;
+using TheLegendOfGustav.Entities;
+using TheLegendOfGustav.Entities.Actors;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// A parte visual do mapa.
+/// </summary>
+public partial class Map : Node2D
+{
+ /// <summary>
+ /// Dados do mapa.
+ /// </summary>
+ public MapData MapData { get; private set; }
+
+ /// <summary>
+ /// raio de alcance da visão do jogador.
+ /// </summary>
+ [Export]
+ private int FovRadius { get; set; } = 12;
+
+ /// <summary>
+ /// Gerador de mapas.
+ /// </summary>
+ private DungeonGenerator Generator { get; set; }
+
+ FieldOfView FieldOfView { get; set; }
+
+ private Node2D TilesNode { get; set; }
+ private Node2D EntitiesNode { get; set; }
+
+ public override void _Ready()
+ {
+ base._Ready();
+ // Começamos obtendo nós relevantes para o mapa.
+ Generator = GetNode<DungeonGenerator>("Generator");
+ FieldOfView = GetNode<FieldOfView>("FieldOfView");
+ TilesNode = GetNode<Node2D>("Tiles");
+ EntitiesNode = GetNode<Node2D>("Entities");
+ }
+
+ /// <summary>
+ /// Cria um andar da masmorra utilizando o gerador de mapa.
+ /// </summary>
+ /// <param name="player">O gerador de mapas precisa do jogador.</param>
+ public void Generate(Player player)
+ {
+ MapData = Generator.GenerateDungeon(player);
+
+ MapData.EntityPlaced += OnEntityPlaced;
+
+ PlaceTiles();
+ PlaceEntities();
+ }
+
+ /// <summary>
+ /// Atualiza o campo de visão do mapa com base em uma coordenada.
+ /// </summary>
+ /// <param name="pos">Centro de visão, normalmente é a posição do jogador.</param>
+ public void UpdateFOV(Vector2I pos)
+ {
+ FieldOfView.UpdateFOV(MapData, pos, FovRadius);
+ // Esconde ou revela entidades com base no campo de visão.
+ foreach (Entity entity in MapData.Entities)
+ {
+ entity.Visible = MapData.GetTile(entity.GridPosition).IsInView;
+ }
+ }
+
+
+ /// <summary>
+ /// Coloca todos os tiles do mapa no mundo do jogo.
+ /// </summary>
+ private void PlaceTiles()
+ {
+ foreach (Tile tile in MapData.Tiles)
+ {
+ TilesNode.AddChild(tile);
+ }
+ }
+
+ /// <summary>
+ /// Coloca todas as entidades do mapa no mundo do jogo.
+ /// </summary>
+ private void PlaceEntities()
+ {
+ foreach (Entity entity in MapData.Entities)
+ {
+ EntitiesNode.AddChild(entity);
+ }
+ }
+
+ private void OnEntityPlaced(Entity entity)
+ {
+ EntitiesNode.AddChild(entity);
+ }
+}
diff --git a/scripts/Map/Map.cs.uid b/scripts/Map/Map.cs.uid
new file mode 100644
index 0000000..7306888
--- /dev/null
+++ b/scripts/Map/Map.cs.uid
@@ -0,0 +1 @@
+uid://fe2h4is11tnw
diff --git a/scripts/Map/MapData.cs b/scripts/Map/MapData.cs
new file mode 100644
index 0000000..3365380
--- /dev/null
+++ b/scripts/Map/MapData.cs
@@ -0,0 +1,318 @@
+using Godot;
+using TheLegendOfGustav.Entities;
+using TheLegendOfGustav.Entities.Actors;
+using TheLegendOfGustav.Entities.Items;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// Classe que cuida dos dados e da parte lógica do mapa.
+/// O mapa é o cenário onde as ações do jogo ocorrem.
+/// Mais especificamente, o mapa é um único andar da masmorra.
+/// </summary>
+public partial class MapData : RefCounted
+{
+ #region Fields
+ public static readonly TileDefinition wallDefinition = GD.Load<TileDefinition>("res://assets/definitions/tiles/wall.tres");
+ public static readonly TileDefinition floorDefinition = GD.Load<TileDefinition>("res://assets/definitions/tiles/floor.tres");
+ /// <summary>
+ /// Peso do ator no pathfinder.
+ /// A IA irá evitar de passar por espaços com peso alto.
+ /// </summary>
+ private static readonly float EntityWeight = 10.0f;
+ #endregion
+
+ #region Constructor
+ public MapData(int width, int height, Player player)
+ {
+ Width = width;
+ Height = height;
+
+ Player = player;
+ // Como o jogador é criado antes do mapa, precisamos
+ // atualizá-lo com o novo mapa.
+ player.MapData = this;
+ InsertEntity(player);
+
+ SetupTiles();
+ }
+ #endregion
+
+ #region Signals
+ [Signal]
+ public delegate void EntityPlacedEventHandler(Entity entity);
+ #endregion
+
+ #region Properties
+ /// <summary>
+ /// Largura do mapa.
+ /// </summary>
+ public int Width { get; private set; }
+ /// <summary>
+ /// Altura do mapa.
+ /// </summary>
+ public int Height { get; private set; }
+
+ /// <summary>
+ /// Os tiles que compõem o mapa.
+ /// </summary>
+ public Godot.Collections.Array<Tile> Tiles { get; private set; } = [];
+
+ /// <summary>
+ /// O jogador é especial e por isso o mapa faz questão de rastreá-lo.
+ /// </summary>
+ public Player Player { get; set; }
+ /// <summary>
+ /// Lista de todas as entidades dentro do mapa.
+ /// </summary>
+ public Godot.Collections.Array<Entity> Entities { get; private set; } = [];
+
+ /// <summary>
+ /// Lista de todos os itens dentro do mapa.
+ /// </summary>
+ public Godot.Collections.Array<ConsumableItem> Items
+ {
+ get
+ {
+ Godot.Collections.Array<ConsumableItem> list = [];
+ foreach (Entity entity in Entities)
+ {
+ if (entity is ConsumableItem item)
+ {
+ list.Add(item);
+ }
+ }
+ return list;
+ }
+ }
+ /// <summary>
+ /// Objeto do Godot que utiliza do algoritmo A* para calcular
+ /// caminhos e rotas.
+ /// </summary>
+ public AStarGrid2D Pathfinder { get; private set; }
+ #endregion
+
+ #region Methods
+ /// <summary>
+ /// Inicializa o pathfinder;
+ /// </summary>
+ public void SetupPathfinding()
+ {
+ Pathfinder = new AStarGrid2D
+ {
+ //A região é o mapa inteiro.
+ Region = new Rect2I(0, 0, Width, Height)
+ };
+
+ // Atualiza o pathfinder para a região definida.
+ Pathfinder.Update();
+
+ // Define quais pontos do mapa são passáveis ou não.
+ for (int y = 0; y < Height; y++)
+ {
+ for (int x = 0; x < Width; x++)
+ {
+ Vector2I pos = new Vector2I(x, y);
+ Tile tile = GetTile(pos);
+ // Pontos sólidos são impossíveis de passar.
+ Pathfinder.SetPointSolid(pos, !tile.IsWalkable);
+ }
+ }
+
+ // Registra todos os atores em cena.
+ foreach (Entity entity in Entities)
+ {
+ if (entity.BlocksMovement)
+ {
+ RegisterBlockingEntity(entity);
+ }
+ }
+
+ }
+
+ /// <summary>
+ /// Define um peso na posição de uma entidade para que a IA evite de passar por lá.
+ /// Ênfase em evitar. Se o único caminho para o destino estiver bloqueado
+ /// por uma entidade, o jogo tentará andar mesmo assim.
+ /// </summary>
+ /// <param name="entity">A entidade em questão.</param>
+ public void RegisterBlockingEntity(Entity entity)
+ {
+ Pathfinder.SetPointWeightScale(entity.GridPosition, EntityWeight);
+ }
+
+ /// <summary>
+ /// Remove o peso na posição de uma entidade.
+ /// Quando uma entidade move sua posição, devemos tirar o peso de sua posição anterior.
+ /// </summary>
+ /// <param name="entity">A entidade em questão.</param>
+ public void UnregisterBlockingEntity(Entity entity)
+ {
+ Pathfinder.SetPointWeightScale(entity.GridPosition, 0);
+ }
+
+ /// <summary>
+ /// Registra uma entidade no mapa. A existência de uma entidade não é considerada se ela não
+ /// estiver registrada no mapa.
+ /// </summary>
+ /// <param name="entity">A entidade em questão</param>
+ public void InsertEntity(Entity entity)
+ {
+ Entities.Add(entity);
+ }
+
+ /// <summary>
+ /// Obtém o tile na posição desejada.
+ /// </summary>
+ /// <param name="pos">Vetor posição</param>
+ /// <returns>O tile na posição, nulo se for fora do mapa.</returns>
+ public Tile GetTile(Vector2I pos)
+ {
+ int index = GridToIndex(pos);
+
+ if (index < 0) return null;
+
+ return Tiles[index];
+ }
+
+ /// <summary>
+ /// Obtém o tile na posição desejada.
+ /// </summary>
+ /// <param name="x">x da coordenada</param>
+ /// <param name="y">y da coordenada</param>
+ /// <returns>O tile na posição, nulo se for fora do mapa.</returns>
+ public Tile GetTile(int x, int y)
+ {
+ return GetTile(new Vector2I(x, y));
+ }
+ /// <summary>
+ /// Obtém a entidade na posição especificada.
+ /// </summary>
+ /// <param name="pos">Vetor posição</param>
+ /// <returns>A entidade na posição especificada, nulo se não houver.</returns>
+ public Entity GetBlockingEntityAtPosition(Vector2I pos)
+ {
+ foreach (Entity entity in Entities)
+ {
+ if (entity.GridPosition == pos && entity.BlocksMovement)
+ {
+ return entity;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Obtém o primeiro item na posição especificada.
+ /// </summary>
+ /// <param name="pos">Posição</param>
+ /// <returns>O primeiro item na posição, nulo se não houver.</returns>
+ public ConsumableItem GetFirstItemAtPosition(Vector2I pos)
+ {
+ foreach (ConsumableItem item in Items)
+ {
+ if (item.GridPosition == pos)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Remove uma entidade do mapa sem dar free.
+ /// </summary>
+ /// <param name="entity">A entidade para remover</param>
+ public void RemoveEntity(Entity entity)
+ {
+ // Eu removo a entidade do nó de entidades.
+ entity.GetParent().RemoveChild(entity);
+ // Eu removo a entidade da lista de entidades do mapa.
+ Entities.Remove(entity);
+ }
+
+ /// <summary>
+ /// Obtém todas as entidades na posição especificada.
+ /// É possível haver mais de uma entidade na mesma posição se uma delas não bloquear movimento.
+ /// </summary>
+ /// <param name="pos">Vetor posição</param>
+ /// <returns>Lista com todas as entidades na posição especificada.</returns>
+ public Godot.Collections.Array<Entity> GetEntitiesAtPosition(Vector2I pos)
+ {
+ Godot.Collections.Array<Entity> ZOfZero = [];
+ Godot.Collections.Array<Entity> ZOfOne = [];
+ Godot.Collections.Array<Entity> ZOfTwo = [];
+
+ // Pego todos os atores
+ foreach (Entity entity in Entities)
+ {
+ if (entity.GridPosition == pos)
+ {
+ switch (entity.ZIndex)
+ {
+ case 0:
+ ZOfZero.Add(entity);
+ break;
+ case 1:
+ ZOfOne.Add(entity);
+ break;
+ case 2:
+ ZOfTwo.Add(entity);
+ break;
+ }
+ }
+ }
+
+ // Retorno os atores ordenados por ZIndex.
+ return ZOfZero + ZOfOne + ZOfTwo;
+ }
+
+ /// <summary>
+ /// Cria novos Tiles até preencher as dimensões do mapa.
+ /// É importante que estes tiles sejam paredes, o gerador de mapas
+ /// não cria paredes por conta própria.
+ /// </summary>
+ private void SetupTiles()
+ {
+ for (int i = 0; i < Height; i++)
+ {
+ for (int j = 0; j < Width; j++)
+ {
+ Tiles.Add(new Tile(new Vector2I(j, i), wallDefinition));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converte uma coordenada em um índice para acessar a lista de tiles.
+ /// </summary>
+ /// <param name="pos">Vetor posição</param>
+ /// <returns>Índice na lista de tiles. -1 se estiver fora do mapa.</returns>
+ private int GridToIndex(Vector2I pos)
+ {
+ if (!IsInBounds(pos)) return -1;
+
+ return pos.Y * Width + pos.X;
+ }
+
+ /// <summary>
+ /// Se uma coordenada está dentro da área do mapa.
+ /// </summary>
+ /// <param name="pos">Vetor posição</param>
+ /// <returns>Se o vetor está dentro do mapa.</returns>
+ private bool IsInBounds(Vector2I pos)
+ {
+ if (pos.X < 0 || pos.Y < 0)
+ {
+ return false;
+ }
+ if (pos.X >= Width || pos.Y >= Height)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ #endregion
+}
diff --git a/scripts/Map/MapData.cs.uid b/scripts/Map/MapData.cs.uid
new file mode 100644
index 0000000..6c226e7
--- /dev/null
+++ b/scripts/Map/MapData.cs.uid
@@ -0,0 +1 @@
+uid://0vbcl1etfcbg
diff --git a/scripts/Map/MapDivision.cs b/scripts/Map/MapDivision.cs
new file mode 100644
index 0000000..e50b331
--- /dev/null
+++ b/scripts/Map/MapDivision.cs
@@ -0,0 +1,113 @@
+using Godot;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// Classe utilizada pelo gerador de mapas.
+/// Uma divisão é uma região retangular de espaço que pode
+/// conter dentro de si duas regiões menores *ou* uma sala.
+/// Uma divisão é uma árvore binária que possui espaço para salas em suas folhas.
+/// </summary>
+public partial class MapDivision : RefCounted
+{
+ public MapDivision(Vector2I position, Vector2I size)
+ {
+ Position = position;
+ Size = size;
+ }
+
+ public MapDivision(Vector2I position, int width, int height)
+ {
+ Position = position;
+ Size = new(width, height);
+ }
+
+ public MapDivision(int x, int y, int width, int height)
+ {
+ Position = new(x, y);
+ Size = new(width, height);
+ }
+
+ /// <summary>
+ /// Região retangular da divisão.
+ /// </summary>
+ public Vector2I Position { get; set; }
+ public Vector2I Size { get; set; }
+
+ public Vector2I Center
+ {
+ get => new(Position.X + Size.X / 2, Position.Y + Size.Y / 2);
+ }
+
+ /// <summary>
+ /// Filhos da árvore
+ /// </summary>
+ public MapDivision Left { get; private set; }
+ public MapDivision Right { get; private set; }
+
+ /// <summary>
+ /// Se a divisão atual for uma folha.
+ /// As folhas representam salas.
+ /// </summary>
+ public bool IsLeaf
+ {
+ get => Left == null && Right == null;
+ }
+
+ /// <summary>
+ /// É conveniente ter acesso à todas as folhas da árvore.
+ /// </summary>
+ /// <returns>Lista com todas as folhas da árvore.</returns>
+ public Godot.Collections.Array<MapDivision> GetLeaves()
+ {
+ if (IsLeaf)
+ {
+ Godot.Collections.Array<MapDivision> list = [];
+ list.Add(this);
+ return list;
+ }
+ return Left.GetLeaves() + Right.GetLeaves();
+ }
+
+ /// <summary>
+ /// Algoritmo para gerar as divisões.
+ /// O mapa começa com uma única divisão que oculpa sua extensão completa.
+ /// Depois disso, ela se dividirá recursivamente n vezes.
+ /// As divisões nas folhas representam espaços onde pode gerar uma sala.
+ /// </summary>
+ /// <param name="iterations">Número de iterações</param>
+ /// <param name="rng">Gerador de números</param>
+ public void Split(int iterations, RandomNumberGenerator rng)
+ {
+ float SplitRatio = rng.RandfRange(0.35f, 0.65f);
+ bool horizontalSplit = Size.X <= Size.Y;
+
+ // Eu defini um limite mínimo de 4 de altura e 4 de largura para divisões.
+
+ if (horizontalSplit)
+ {
+ int leftHeight = (int)(Size.Y * SplitRatio);
+ if (leftHeight > 4 && Size.Y - leftHeight > 4)
+ {
+ Left = new MapDivision(Position, Size.X, leftHeight);
+ Right = new MapDivision(Position.X, Position.Y + leftHeight, Size.X, Size.Y - leftHeight);
+ }
+ }
+ else
+ {
+ int leftWidth = (int)(Size.Y * SplitRatio);
+
+ if (leftWidth > 4 && Size.Y - leftWidth > 4)
+ {
+ Left = new MapDivision(Position, leftWidth, Size.Y);
+ Right = new MapDivision(Position.X + leftWidth, Position.Y, Size.X - leftWidth, Size.Y);
+ }
+ }
+
+ if (iterations > 1)
+ {
+ Left?.Split(iterations - 1, rng);
+ Right?.Split(iterations - 1, rng);
+ }
+ }
+} \ No newline at end of file
diff --git a/scripts/Map/MapDivision.cs.uid b/scripts/Map/MapDivision.cs.uid
new file mode 100644
index 0000000..fdd53a4
--- /dev/null
+++ b/scripts/Map/MapDivision.cs.uid
@@ -0,0 +1 @@
+uid://c3niegr686acj
diff --git a/scripts/Map/Tile.cs b/scripts/Map/Tile.cs
new file mode 100644
index 0000000..e721fca
--- /dev/null
+++ b/scripts/Map/Tile.cs
@@ -0,0 +1,90 @@
+using Godot;
+using TheLegendOfGustav.Utils;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// O mundo do jogo é composto por Tiles.
+/// Um tile é um quadrado de 16x16 que representa uma
+/// unidade discreta do cenário. Tiles podem agir como
+/// parede, chão, ou outras funções.
+/// </summary>
+public partial class Tile : Sprite2D
+{
+ private bool isExplored = false;
+ private bool isInView = false;
+
+ public Tile(Vector2I pos, TileDefinition definition)
+ {
+ // Tile herda da classe Sprite2D.
+ // Por padrão, a posição do Sprite2D é no centro de sua textura.
+ // Para o jogo, faz mais sentido que a posição seja no
+ // canto superior esquerdo.
+ Centered = false;
+ // Tiles começam invisíveis porque não foram vistos pelo jogador.
+ Visible = false;
+ Position = Grid.GridToWorld(pos);
+ SetDefinition(definition);
+ }
+
+ /// <summary>
+ /// Determina se atores podem andar em cima do Tile.
+ /// </summary>
+ public bool IsWalkable { get; private set; }
+ /// <summary>
+ /// Determina se o tile bloqueia visão.
+ /// </summary>
+ public bool IsTransparent { get; private set; }
+
+ /// <summary>
+ /// A definição do tile carrega seus valores padrão.
+ /// </summary>
+ private TileDefinition Definition { get; set; }
+ /// <summary>
+ /// Se o jogador já viu este tile antes.
+ /// Tiles não descobertos são invisíveis.
+ /// </summary>
+ public bool IsExplored
+ {
+ get => isExplored;
+ set
+ {
+ isExplored = value;
+ if (IsExplored && !Visible)
+ {
+ Visible = true;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Se o jogador vê o tile neste exato momento.
+ /// Elementos neste tile estão dentro do campo de visão do jogador.
+ /// </summary>
+ public bool IsInView
+ {
+ get => isInView;
+ set
+ {
+ isInView = value;
+ Modulate = isInView ? Definition.LitColor : Definition.DarkColor;
+ if (IsInView && !IsExplored)
+ {
+ IsExplored = true;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Define as características do tile.
+ /// </summary>
+ /// <param name="definition">Definição do tile.</param>
+ public void SetDefinition(TileDefinition definition)
+ {
+ Definition = definition;
+ Modulate = definition.DarkColor;
+ Texture = definition.Texture;
+ IsWalkable = definition.IsWalkable;
+ IsTransparent = definition.IsTransparent;
+ }
+}
diff --git a/scripts/Map/Tile.cs.uid b/scripts/Map/Tile.cs.uid
new file mode 100644
index 0000000..9fa3c81
--- /dev/null
+++ b/scripts/Map/Tile.cs.uid
@@ -0,0 +1 @@
+uid://b2dkecc01tqc0
diff --git a/scripts/Map/TileDefinition.cs b/scripts/Map/TileDefinition.cs
new file mode 100644
index 0000000..e843b52
--- /dev/null
+++ b/scripts/Map/TileDefinition.cs
@@ -0,0 +1,24 @@
+using Godot;
+
+namespace TheLegendOfGustav.Map;
+
+/// <summary>
+/// Define as características de um tile.
+/// </summary>
+[GlobalClass]
+public partial class TileDefinition : Resource
+{
+ [ExportCategory("Visuals")]
+ [Export]
+ public Texture2D Texture { get; set; }
+ [Export(PropertyHint.ColorNoAlpha)]
+ public Color LitColor { get; set; } = Colors.White;
+ [Export(PropertyHint.ColorNoAlpha)]
+ public Color DarkColor { get; set; } = Colors.White;
+
+ [ExportCategory("Mechanics")]
+ [Export]
+ public bool IsWalkable { get; set; }
+ [Export]
+ public bool IsTransparent { get; set; }
+}
diff --git a/scripts/Map/TileDefinition.cs.uid b/scripts/Map/TileDefinition.cs.uid
new file mode 100644
index 0000000..14f2903
--- /dev/null
+++ b/scripts/Map/TileDefinition.cs.uid
@@ -0,0 +1 @@
+uid://ba82a33ov6uuo