RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
battlefield_generator.gd
Go to the documentation of this file.
1extends RefCounted
2
3
6
7
8func generate(prototype:Dictionary, options := {}) -> Dictionary:
9 var rng = RandomNumberGenerator.new()
10 var seed = int(options.get("seed", Time.get_ticks_msec()))
11 rng.seed = seed
12 var mode = str(options.get("mode", "arena"))
13 if mode == "battle":
14 return _generate_battle(prototype, options, rng, seed)
15 return _generate_arena(prototype, options, rng, seed)
16
17
18func _generate_arena(prototype:Dictionary, options:Dictionary, rng:RandomNumberGenerator, seed:int) -> Dictionary:
19 var world = prototype.get("world", {}).duplicate(true)
20 var grid_size = _cell_from_value(world.get("grid_size", [12, 12]))
21 var player_cell = _cell_from_value(prototype.get("player", {}).get("grid", [2, 2]))
22 var exit_cell = _cell_from_value(prototype.get("exit", {}).get("cell", [grid_size.x - 1, grid_size.y - 1]))
23 var blocked := {player_cell: true, exit_cell: true}
24 for direction in [Vector2i.LEFT, Vector2i.RIGHT, Vector2i.UP, Vector2i.DOWN]:
25 var safe_cell = player_cell + direction
26 if safe_cell.x >= 0 and safe_cell.y >= 0 and safe_cell.x < grid_size.x and safe_cell.y < grid_size.y:
27 blocked[safe_cell] = true
28 var arena_options = prototype.get("arena", {})
29 world["obstacles"] = _generate_obstacles(rng, grid_size, blocked, int(options.get("obstacles", arena_options.get("obstacles", 5))))
30 var obstacle_cells := {}
31 for obstacle in world.get("obstacles", []):
32 for cell in obstacle.get("cells", []):
33 obstacle_cells[_cell_from_value(cell)] = true
34 for cell in obstacle_cells.keys():
35 blocked[cell] = true
36 world["material_nodes"] = _place_materials(rng, world.get("material_nodes", []), grid_size, blocked)
37 for node in world.get("material_nodes", []):
38 blocked[_cell_from_value(node.get("cell", [0, 0]))] = true
39 world["spell_interactions"] = _place_interactions(rng, world.get("spell_interactions", []), grid_size, blocked)
40 for interaction in world.get("spell_interactions", []):
41 blocked[_cell_from_value(interaction.get("cell", [0, 0]))] = true
42 var enemy_pool = _arena_enemy_pool(prototype.get("enemies", []), arena_options)
43 var enemy_min = max(0, int(arena_options.get("enemy_count_min", min(1, enemy_pool.size()))))
44 var enemy_max = max(enemy_min, int(arena_options.get("enemy_count_max", enemy_pool.size())))
45 _shuffle_values(enemy_pool, rng)
46 var enemy_count = min(enemy_pool.size(), rng.randi_range(enemy_min, enemy_max) if enemy_max > 0 else 0)
47 var enemies = _place_enemies(rng, enemy_pool.slice(0, enemy_count), grid_size, blocked)
48 return {
49 "mode": "arena",
50 "seed": seed,
51 "world": world,
52 "enemies": enemies,
53 "player_grid": [player_cell.x, player_cell.y]
54 }
55
56
57func _generate_battle(prototype:Dictionary, options:Dictionary, rng:RandomNumberGenerator, seed:int) -> Dictionary:
58 var half_size = clamp(int(options.get("half_size", 3)), 3, 5)
59 var grid_size = Vector2i(half_size * 2, half_size)
60 var tile_size = float(prototype.get("world", {}).get("tile_size", 40.0))
61 var player_cell = Vector2i(max(0, int(half_size / 2)), max(0, int(half_size / 2)))
62 var exit_cell = Vector2i(0, max(0, half_size - 1))
63 var world = prototype.get("world", {}).duplicate(true)
64 world["grid_size"] = [grid_size.x, grid_size.y]
65 world["tile_size"] = tile_size
66 world["obstacles"] = []
67 world["material_nodes"] = []
68 world["spell_interactions"] = []
69 world["regions"] = [
70 {"id": "player_side", "name": "Player Half", "cells": [0, 0, half_size, half_size], "color": "#172834"},
71 {"id": "enemy_side", "name": "Enemy Half", "cells": [half_size, 0, half_size, half_size], "color": "#301820"}
72 ]
73 world["camera"] = {"viewport_tiles": [grid_size.x, max(5, grid_size.y)]}
74 var blocked := {player_cell: true, exit_cell: true}
75 var enemy_cells := []
76 for y in range(grid_size.y):
77 for x in range(half_size, grid_size.x):
78 enemy_cells.append(Vector2i(x, y))
79 _shuffle_cells(enemy_cells, rng)
80 var enemies := []
81 var enemy_pool = prototype.get("enemies", []).duplicate(true)
82 _shuffle_values(enemy_pool, rng)
83 var enemy_count = min(enemy_pool.size(), rng.randi_range(1, half_size))
84 for index in range(enemy_count):
85 var enemy = enemy_pool[index]
86 if not enemy is Dictionary or enemy_cells.is_empty():
87 continue
88 var cell = enemy_cells.pop_back()
89 if blocked.has(cell):
90 continue
91 blocked[cell] = true
92 var entry = enemy.duplicate(true)
93 entry["cell"] = [cell.x, cell.y]
94 enemies.append(entry)
95 return {
96 "mode": "battle",
97 "half_size": half_size,
98 "seed": seed,
99 "world": world,
100 "enemies": enemies,
101 "player_grid": [player_cell.x, player_cell.y],
102 "exit": {
103 "cell": [exit_cell.x, exit_cell.y],
104 "active_after_enemies_defeated": true,
105 "label": "Exit"
106 }
107 }
108
109
110func _generate_obstacles(rng:RandomNumberGenerator, grid_size:Vector2i, blocked:Dictionary, count:int) -> Array:
111 var obstacles := []
112 var attempts := 0
113 while obstacles.size() < count and attempts < count * 30:
114 attempts += 1
115 var cell = Vector2i(rng.randi_range(1, max(1, grid_size.x - 2)), rng.randi_range(1, max(1, grid_size.y - 2)))
116 if blocked.has(cell):
117 continue
118 blocked[cell] = true
119 obstacles.append({
120 "id": "generated_obstacle_%d" % obstacles.size(),
121 "name": "Field Obstacle",
122 "cells": [[cell.x, cell.y]]
123 })
124 return obstacles
125
126
127func _place_materials(rng:RandomNumberGenerator, nodes:Array, grid_size:Vector2i, blocked:Dictionary) -> Array:
128 var placed := []
129 for node in nodes:
130 if not node is Dictionary:
131 continue
132 var cell = _random_open_cell(rng, grid_size, blocked)
133 if cell == Vector2i(-1, -1):
134 continue
135 blocked[cell] = true
136 var entry = node.duplicate(true)
137 entry["cell"] = [cell.x, cell.y]
138 placed.append(entry)
139 return placed
140
141
142func _place_interactions(rng:RandomNumberGenerator, interactions:Array, grid_size:Vector2i, blocked:Dictionary) -> Array:
143 var placed := []
144 for interaction in interactions:
145 if not interaction is Dictionary:
146 continue
147 var cell = _random_open_cell(rng, grid_size, blocked)
148 if cell == Vector2i(-1, -1):
149 continue
150 blocked[cell] = true
151 var entry = interaction.duplicate(true)
152 entry["cell"] = [cell.x, cell.y]
153 placed.append(entry)
154 return placed
155
156
157func _place_enemies(rng:RandomNumberGenerator, enemies:Array, grid_size:Vector2i, blocked:Dictionary) -> Array:
158 var placed := []
159 for enemy in enemies:
160 if not enemy is Dictionary:
161 continue
162 var cell = _random_open_cell(rng, grid_size, blocked)
163 if cell == Vector2i(-1, -1):
164 continue
165 blocked[cell] = true
166 var entry = enemy.duplicate(true)
167 entry["cell"] = [cell.x, cell.y]
168 placed.append(entry)
169 return placed
170
171
172func _arena_enemy_pool(enemies:Array, arena_options:Dictionary) -> Array:
173 var allowed = arena_options.get("enemy_archetypes", [])
174 var pool := []
175 for enemy in enemies:
176 if not enemy is Dictionary:
177 continue
178 if allowed is Array and not allowed.is_empty() and not allowed.has(str(enemy.get("archetype", ""))):
179 continue
180 pool.append(enemy.duplicate(true))
181 return pool
182
183
184func _random_open_cell(rng:RandomNumberGenerator, grid_size:Vector2i, blocked:Dictionary) -> Vector2i:
185 for _attempt in range(200):
186 var cell = Vector2i(rng.randi_range(0, grid_size.x - 1), rng.randi_range(0, grid_size.y - 1))
187 if not blocked.has(cell):
188 return cell
189 return Vector2i(-1, -1)
190
191
192func _shuffle_cells(cells:Array, rng:RandomNumberGenerator) -> void:
193 for index in range(cells.size() - 1, 0, -1):
194 var swap_index = rng.randi_range(0, index)
195 var temp = cells[index]
196 cells[index] = cells[swap_index]
197 cells[swap_index] = temp
198
199
200func _shuffle_values(values:Array, rng:RandomNumberGenerator) -> void:
201 for index in range(values.size() - 1, 0, -1):
202 var swap_index = rng.randi_range(0, index)
203 var temp = values[index]
204 values[index] = values[swap_index]
205 values[swap_index] = temp
206
207
208func _cell_from_value(value) -> Vector2i:
209 if value is Vector2i:
210 return value
211 if value is Vector2:
212 return Vector2i(int(value.x), int(value.y))
213 if value is Array and value.size() >= 2:
214 return Vector2i(int(value[0]), int(value[1]))
215 return Vector2i.ZERO
216
217