diff options
Diffstat (limited to 'scripts/Map')
| -rw-r--r-- | scripts/Map/DungeonGenerator.cs | 301 | ||||
| -rw-r--r-- | scripts/Map/DungeonGenerator.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/FieldOfView.cs | 115 | ||||
| -rw-r--r-- | scripts/Map/FieldOfView.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/Map.cs | 98 | ||||
| -rw-r--r-- | scripts/Map/Map.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/MapData.cs | 318 | ||||
| -rw-r--r-- | scripts/Map/MapData.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/MapDivision.cs | 113 | ||||
| -rw-r--r-- | scripts/Map/MapDivision.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/Tile.cs | 90 | ||||
| -rw-r--r-- | scripts/Map/Tile.cs.uid | 1 | ||||
| -rw-r--r-- | scripts/Map/TileDefinition.cs | 24 | ||||
| -rw-r--r-- | scripts/Map/TileDefinition.cs.uid | 1 |
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 |
