1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
using Godot;
/// <summary>
/// A classe dungeonGenerator cria exatamente um andar da masmorra.
/// Ela é chamada quando necessário.
/// </summary>
public partial class DungeonGenerator : Node
{
/// <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")
];
/// <summary>
/// Dimensões do mapa a ser criado.
/// </summary>
[ExportCategory("Dimension")]
[Export]
private int width = 80;
[Export]
private int height = 60;
/// <summary>
/// Gerador de números aleatórios
/// </summary>
[ExportCategory("RNG")]
private RandomNumberGenerator rng = new();
/// <summary>
/// Qual seed utilizar.
/// </summary>
[Export]
private ulong seed;
/// <summary>
/// Se será utilizada a nossa seed ou a seed padrão da classe RandomNumberGenerator.
/// </summary>
[Export]
private bool useSeed = true;
/// <summary>
/// Quantas iterações do algoritmo chamar.
/// </summary>
[Export]
private int iterations = 3;
/// <summary>
/// Quantidade máxima de inimigos por sala.
/// </summary>
[ExportCategory("Monster RNG")]
[Export]
private int maxMonsterPerRoom = 2;
/// <summary>
/// Quantidade máxima de itens por sala.
/// </summary>
[ExportCategory("Loot RNG")]
[Export]
private int maxItemsPerRoom = 2;
public override void _Ready()
{
base._Ready();
if (useSeed) {
rng.Seed = seed;
}
}
/// <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>
/// 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>
/// 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);
}
}
}
}
/// <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>
/// 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>
/// 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);
}
}
|