11const TOOLS := [
"tile",
"object",
"erase"]
14func create_history(max_entries:int = 40) -> Dictionary:
18 "max_entries": max(1, max_entries),
23func push_history(history:Dictionary, level_data:Dictionary) -> void:
24 if bool(history.get(
"restoring", false))
or level_data.is_empty():
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:
31 history[
"undo"] = undo
35func can_undo(history:Dictionary) -> bool:
36 return not (history.get(
"undo", [])
as Array).is_empty()
39func can_redo(history:Dictionary) -> bool:
40 return not (history.get(
"redo", [])
as Array).is_empty()
43func undo(history:Dictionary, current_data:Dictionary) -> Dictionary:
44 var undo_stack:Array = history.get(
"undo", [])
45 if undo_stack.is_empty():
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()
54func redo(history:Dictionary, current_data:Dictionary) -> Dictionary:
55 var redo_stack:Array = history.get(
"redo", [])
56 if redo_stack.is_empty():
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()
65func normalized_rows(rows, width:int, height:int, fill_tile :=
".") -> Array:
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)
77func dimensions_from_level(level_data:Dictionary) -> Vector2i:
78 var layers = get_layers(level_data)
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())
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())
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
95func clamp_cell(level_data:Dictionary, cell:Vector2i) -> Vector2i:
96 var dimensions = dimensions_from_level(level_data)
97 if dimensions == Vector2i.ZERO:
100 clampi(cell.x, 0, max(0, dimensions.x - 1)),
101 clampi(cell.y, 0, max(0, dimensions.y - 1))
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", [])
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", [])
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
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):
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
145func remove_objects_at_cell(level_data:Dictionary, cell:Vector2i) -> Dictionary:
146 var result = level_data.duplicate(true)
148 for object
in get_objects(result):
149 if object
is Dictionary
and cell_from_tile(object.get(
"tile", [])) == cell:
151 objects.append(object)
152 result[
"objects"] = objects
156func remove_object_by_id(level_data:Dictionary, object_id:String) -> Dictionary:
157 var result = level_data.duplicate(true)
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
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}
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():
189 var row = str(rows[cell.y])
190 if row[cell.x] != target_tile:
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}
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():
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():
211 var row = str(rows[cell.y])
212 if cell.x < 0
or cell.x >= row.length():
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())])
222func palette_visibility(tool:String) -> Dictionary:
224 "tiles": tool ==
"tile",
225 "objects": tool ==
"object",
226 "object_panel": tool ==
"object"
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"
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"}
250func cell_from_tile(tile_value) -> Vector2i:
251 if tile_value
is Vector2i:
253 if tile_value
is Vector2:
255 if tile_value
is Array
and tile_value.size() >= 2: