RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
level_editor_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
10
11const TOOLS := ["tile", "object", "erase"]
12
13
14func create_history(max_entries:int = 40) -> Dictionary:
15 return {
16 "undo": [],
17 "redo": [],
18 "max_entries": max(1, max_entries),
19 "restoring": false
20 }
21
22
23func push_history(history:Dictionary, level_data:Dictionary) -> void:
24 if bool(history.get("restoring", false)) or level_data.is_empty():
25 return
26 var undo:Array = history.get("undo", [])
27 undo.append(level_data.duplicate(true))
28 var max_entries = int(history.get("max_entries", 40))
29 while undo.size() > max_entries:
30 undo.pop_front()
31 history["undo"] = undo
32 history["redo"] = []
33
34
35func can_undo(history:Dictionary) -> bool:
36 return not (history.get("undo", []) as Array).is_empty()
37
38
39func can_redo(history:Dictionary) -> bool:
40 return not (history.get("redo", []) as Array).is_empty()
41
42
43func undo(history:Dictionary, current_data:Dictionary) -> Dictionary:
44 var undo_stack:Array = history.get("undo", [])
45 if undo_stack.is_empty():
46 return {}
47 var redo_stack:Array = history.get("redo", [])
48 redo_stack.append(current_data.duplicate(true))
49 history["redo"] = redo_stack
50 history["undo"] = undo_stack
51 return undo_stack.pop_back()
52
53
54func redo(history:Dictionary, current_data:Dictionary) -> Dictionary:
55 var redo_stack:Array = history.get("redo", [])
56 if redo_stack.is_empty():
57 return {}
58 var undo_stack:Array = history.get("undo", [])
59 undo_stack.append(current_data.duplicate(true))
60 history["undo"] = undo_stack
61 history["redo"] = redo_stack
62 return redo_stack.pop_back()
63
64
65func normalized_rows(rows, width:int, height:int, fill_tile := ".") -> Array:
66 var result := []
67 for y in range(max(0, height)):
68 var row = str(rows[y]) if rows is Array and y < rows.size() else ""
69 if row.length() < width:
70 row += fill_tile.repeat(width - row.length())
71 elif row.length() > width:
72 row = row.substr(0, width)
73 result.append(row)
74 return result
75
76
77func dimensions_from_level(level_data:Dictionary) -> Vector2i:
78 var layers = get_layers(level_data)
79 if layers.is_empty():
80 var legacy_tiles = level_data.get("tiles", [])
81 if legacy_tiles is Array and not legacy_tiles.is_empty():
82 return Vector2i(str(legacy_tiles[0]).length(), legacy_tiles.size())
83 return Vector2i.ZERO
84 var rows = layers[0].get("tiles", [])
85 if rows is Array and not rows.is_empty():
86 return Vector2i(str(rows[0]).length(), rows.size())
87 return Vector2i.ZERO
88
89
90func cell_in_bounds(level_data:Dictionary, cell:Vector2i) -> bool:
91 var dimensions = dimensions_from_level(level_data)
92 return cell.x >= 0 and cell.y >= 0 and cell.x < dimensions.x and cell.y < dimensions.y
93
94
95func clamp_cell(level_data:Dictionary, cell:Vector2i) -> Vector2i:
96 var dimensions = dimensions_from_level(level_data)
97 if dimensions == Vector2i.ZERO:
98 return Vector2i.ZERO
99 return Vector2i(
100 clampi(cell.x, 0, max(0, dimensions.x - 1)),
101 clampi(cell.y, 0, max(0, dimensions.y - 1))
102 )
103
104
105func get_layers(level_data:Dictionary) -> Array:
106 if not level_data.get("layers", []) is Array:
107 level_data["layers"] = []
108 return level_data.get("layers", [])
109
110
111func get_objects(level_data:Dictionary) -> Array:
112 if not level_data.get("objects", []) is Array:
113 level_data["objects"] = []
114 return level_data.get("objects", [])
115
116
117func resize_layers(level_data:Dictionary, width:int, height:int) -> Dictionary:
118 var result = level_data.duplicate(true)
119 var layers = get_layers(result)
120 for i in range(layers.size()):
121 var layer = layers[i]
122 layer["tiles"] = normalized_rows(layer.get("tiles", []), width, height)
123 result["layers"][i] = layer
124 return result
125
126
127func set_tile(level_data:Dictionary, cell:Vector2i, tile:String, layer_index:int, width:int, height:int) -> Dictionary:
128 var result = level_data.duplicate(true)
129 if not cell_in_bounds(result, cell):
130 return result
131 var layers = get_layers(result)
132 if layers.is_empty():
133 result["layers"] = [{"id": "terrain", "type": "tiles", "collision": true, "visible": true, "tiles": []}]
134 layers = get_layers(result)
135 layer_index = clampi(layer_index, 0, layers.size() - 1)
136 var layer = layers[layer_index]
137 var rows = normalized_rows(layer.get("tiles", []), width, height)
138 var row = str(rows[cell.y])
139 rows[cell.y] = row.substr(0, cell.x) + tile.substr(0, 1) + row.substr(cell.x + 1)
140 layer["tiles"] = rows
141 result["layers"][layer_index] = layer
142 return result
143
144
145func remove_objects_at_cell(level_data:Dictionary, cell:Vector2i) -> Dictionary:
146 var result = level_data.duplicate(true)
147 var objects := []
148 for object in get_objects(result):
149 if object is Dictionary and cell_from_tile(object.get("tile", [])) == cell:
150 continue
151 objects.append(object)
152 result["objects"] = objects
153 return result
154
155
156func remove_object_by_id(level_data:Dictionary, object_id:String) -> Dictionary:
157 var result = level_data.duplicate(true)
158 var objects := []
159 for object in get_objects(result):
160 if object is Dictionary and str(object.get("id", "")) != object_id:
161 objects.append(object)
162 result["objects"] = objects
163 return result
164
165
166func fill_tiles(level_data:Dictionary, start:Vector2i, selected_tile:String, layer_index:int, width:int, height:int) -> Dictionary:
167 var result = level_data.duplicate(true)
168 var layers = get_layers(result)
169 if layers.is_empty():
170 return {"changed": false, "level_data": result, "count": 0, "picked_tile": ""}
171 layer_index = clampi(layer_index, 0, layers.size() - 1)
172 var layer = layers[layer_index]
173 var rows = normalized_rows(layer.get("tiles", []), width, height)
174 if start.y < 0 or start.y >= rows.size():
175 return {"changed": false, "level_data": result, "count": 0, "picked_tile": ""}
176 var start_row = str(rows[start.y])
177 if start.x < 0 or start.x >= start_row.length():
178 return {"changed": false, "level_data": result, "count": 0, "picked_tile": ""}
179 var target_tile = start_row[start.x]
180 if target_tile == selected_tile:
181 return {"changed": false, "level_data": result, "count": 0, "picked_tile": target_tile}
182 var visited := {}
183 var queue := [start]
184 while not queue.is_empty():
185 var cell:Vector2i = queue.pop_front()
186 var key = "%d,%d" % [cell.x, cell.y]
187 if visited.has(key) or cell.x < 0 or cell.y < 0 or cell.y >= rows.size() or cell.x >= start_row.length():
188 continue
189 var row = str(rows[cell.y])
190 if row[cell.x] != target_tile:
191 continue
192 visited[key] = true
193 rows[cell.y] = row.substr(0, cell.x) + selected_tile.substr(0, 1) + row.substr(cell.x + 1)
194 queue.append(cell + Vector2i.LEFT)
195 queue.append(cell + Vector2i.RIGHT)
196 queue.append(cell + Vector2i.UP)
197 queue.append(cell + Vector2i.DOWN)
198 layer["tiles"] = rows
199 result["layers"][layer_index] = layer
200 return {"changed": true, "level_data": result, "count": visited.size(), "picked_tile": target_tile}
201
202
203func pick_tile(level_data:Dictionary, cell:Vector2i, layer_index:int, width:int, height:int) -> String:
204 var layers = get_layers(level_data)
205 if layers.is_empty():
206 return ""
207 layer_index = clampi(layer_index, 0, layers.size() - 1)
208 var rows = normalized_rows(layers[layer_index].get("tiles", []), width, height)
209 if cell.y < 0 or cell.y >= rows.size():
210 return ""
211 var row = str(rows[cell.y])
212 if cell.x < 0 or cell.x >= row.length():
213 return ""
214 return row[cell.x]
215
216
217func cycle_tool(current_tool:String, delta:int = 1) -> String:
218 var index = TOOLS.find(current_tool)
219 return str(TOOLS[posmod(max(0, index) + delta, TOOLS.size())])
220
221
222func palette_visibility(tool:String) -> Dictionary:
223 return {
224 "tiles": tool == "tile",
225 "objects": tool == "object",
226 "object_panel": tool == "object"
227 }
228
229
230func input_badges(controller_active:bool) -> Array:
231 var place = "A" if controller_active else "Enter"
232 var erase = "X" if controller_active else "2"
233 var tool = "Y" if controller_active else "3"
234 var save = "B" if controller_active else "4"
235 var palette = "LB/RB" if controller_active else "PgUp/PgDn"
236 return [
237 {"text": "✚", "tooltip": "Move cursor"},
238 {"text": "%s ◆" % place, "tooltip": "Place"},
239 {"text": "%s ⌫" % erase, "tooltip": "Erase"},
240 {"text": "%s ⚒" % tool, "tooltip": "Cycle tool"},
241 {"text": "%s ▦" % palette, "tooltip": "Cycle palette"},
242 {"text": "F ▣", "tooltip": "Fill area"},
243 {"text": "Q ◉", "tooltip": "Pick tile"},
244 {"text": "Ctrl+Z ↶", "tooltip": "Undo"},
245 {"text": "Ctrl+Y ↷", "tooltip": "Redo"},
246 {"text": "%s ▤" % save, "tooltip": "Save"}
247 ]
248
249
250func cell_from_tile(tile_value) -> Vector2i:
251 if tile_value is Vector2i:
252 return tile_value
253 if tile_value is Vector2:
254 return Vector2i(int(tile_value.x), int(tile_value.y))
255 if tile_value is Array and tile_value.size() >= 2:
256 return Vector2i(int(tile_value[0]), int(tile_value[1]))
257 return Vector2i.ZERO