8const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
17var tile_dimensions_cache :=
Vector2i(-1, -1)
18var rects_by_type_cache := {}
19var tile_records_cache := {}
22var data_cache_manager = DataCacheManager.new()
26func load_level(file_path:String) -> void:
28 var data = data_cache_manager.load_json(file_path)
29 if not data
is Dictionary:
31 level = data.duplicate(true)
32 tile_size = _parse_tile_size(level.get(
"tile_size", [32, 32]))
33 legend = level.get(
"legend", {}).duplicate(true)
34 metadata = level.get(
"metadata", level.get(
"background", {})).duplicate(true)
if level.get(
"metadata", level.get(
"background", {}))
is Dictionary
else {}
35 layers = _normalize_layers(level)
36 objects = _normalize_objects(level.get(
"objects", []), level.get(
"spawns", []))
37 triggers = level.get(
"triggers", []).duplicate(true)
if level.get(
"triggers", [])
is Array
else []
41func get_tile_dimensions() -> Vector2i:
42 if tile_dimensions_cache.x >= 0
and tile_dimensions_cache.y >= 0:
44 return tile_dimensions_cache
49 if not layer
is Dictionary:
51 for row
in layer.get(
"tiles", []):
52 width = max(width, str(row).length())
53 height = max(height, layer.get(
"tiles", []).size())
54 tile_dimensions_cache =
Vector2i(width, height)
55 return tile_dimensions_cache
59func get_pixel_dimensions() -> Vector2:
60 var tile_dimensions = get_tile_dimensions()
61 return Vector2(tile_dimensions.x * tile_size.x, tile_dimensions.y * tile_size.y)
65func get_tile(x:int, y:int, layer_id:String =
"") -> String:
66 var layer = _get_layer(layer_id)
69 var layer_tiles = layer.get(
"tiles", [])
70 if y < 0
or y >= layer_tiles.size():
72 var row = str(layer_tiles[y])
73 if x < 0
or x >= row.length():
79func get_tile_metadata(tile:String) -> Dictionary:
80 return legend.get(tile, {}).duplicate(true)
84func get_rects_by_type(type:String) -> Array:
85 if rects_by_type_cache.has(type):
87 return rects_by_type_cache.get(type, []).duplicate(true)
91 if not bool(layer.get(
"collision", true)):
93 var layer_tiles = layer.get(
"tiles", [])
94 for y
in range(layer_tiles.size()):
95 var row = str(layer_tiles[y])
96 for x
in range(row.length()):
98 var tile_metadata = get_tile_metadata(tile)
99 if str(tile_metadata.get(
"type",
"")) == type:
101 rects_by_type_cache[type] = rects
102 return rects.duplicate(true)
106func get_tile_records(options:Dictionary = {}) -> Array:
107 var include_empty =
bool(options.get(
"include_empty", false))
108 var records = _tile_records_for_include_empty(include_empty)
109 var world_rect = options.get(
"world_rect",
Rect2())
110 if not world_rect
is Rect2
or world_rect.size.x <= 0.0
or world_rect.size.y <= 0.0:
111 return records.duplicate(true)
112 var margin =
float(options.get(
"margin", 0.0))
113 var visible_rect:Rect2 = world_rect.grow(margin)
if margin != 0.0
else world_rect
114 var visible_records := []
115 for record
in records:
116 if _record_intersects_world_rect(record, visible_rect):
117 visible_records.append(record)
118 return visible_records.duplicate(true)
122func build_tile_draw_records(tile_rows:Array, tile_definitions:Dictionary = {}, options:Dictionary = {}) -> Array:
123 var include_empty:bool =
bool(options.get(
"include_empty", true))
124 var empty_symbols:Array = options.get(
"empty_symbols", [
" "])
if options.get(
"empty_symbols", [])
is Array
else [
" "]
125 var records:Array = []
126 for y
in range(tile_rows.size()):
127 var row_text:String = str(tile_rows[y])
128 for x
in range(row_text.length()):
129 var symbol:String = row_text.substr(x, 1)
130 if not include_empty
and empty_symbols.has(symbol):
132 var tile:Dictionary = tile_definitions.get(symbol, {})
if tile_definitions.get(symbol, {})
is Dictionary
else {}
133 var metadata:Dictionary = tile.get(
"metadata", {})
if tile.get(
"metadata", {})
is Dictionary
else {}
137 "tile": tile.duplicate(true),
138 "metadata": metadata.duplicate(true)
148func compile_grid_runtime(tile_rows:Array, tile_definitions:Dictionary = {}, objects:Array = [], options:Dictionary = {}) -> Dictionary:
150 var include_empty:bool =
bool(options.get(
"include_empty", true))
151 var empty_symbols:Array = options.get(
"empty_symbols", [
" "])
if options.get(
"empty_symbols", [])
is Array
else [
" "]
152 var surface_colors:Dictionary = options.get(
"surface_colors", {})
if options.get(
"surface_colors", {})
is Dictionary
else {}
153 var raw_records:Array = build_tile_draw_records(tile_rows, tile_definitions, {
154 "include_empty": include_empty,
155 "empty_symbols": empty_symbols
157 var records:Array = []
158 var colliders:Array = []
159 var hazards:Array = []
160 var by_cell:Dictionary = {}
162 for y
in range(tile_rows.size()):
163 width = max(width, str(tile_rows[y]).length())
164 for raw_record
in raw_records:
165 if not raw_record
is Dictionary:
167 var record:Dictionary = _compile_tile_runtime_record(raw_record, runtime_tile_size, surface_colors)
168 var symbol:String = str(record.get(
"symbol",
""))
169 if not include_empty
and empty_symbols.has(symbol):
171 records.append(record)
172 by_cell[_cell_key(record.get(
"cell", Vector2i.ZERO))] = record
173 if str(record.get(
"type",
"")) ==
"solid":
174 colliders.append(record)
175 elif str(record.get(
"type",
"")) ==
"hazard":
176 hazards.append(record)
177 var fill_runs:Array = build_tile_fill_runs(records, [
"color"], {
178 "include_empty": include_empty,
179 "empty_symbols": empty_symbols
183 "tile_size": runtime_tile_size,
184 "dimensions":
Vector2i(width, tile_rows.size()),
185 "tile_records": records,
186 "tile_fill_runs": fill_runs,
187 "solid_records": colliders,
188 "hazard_records": hazards,
189 "records_by_cell": by_cell,
190 "object_draw_records": build_object_draw_records(objects, options)
200func compile_loaded_runtime(layer_id:String =
"", options:Dictionary = {}) -> Dictionary:
201 var layer:Dictionary = _get_layer(layer_id)
204 var tile_rows:Array = layer.get(
"tiles", [])
if layer.get(
"tiles", [])
is Array
else []
205 var tile_definitions:Dictionary = {}
206 for symbol
in legend.keys():
207 var metadata_value = legend.get(symbol, {})
208 var metadata:Dictionary = metadata_value
if metadata_value
is Dictionary
else {}
209 tile_definitions[str(symbol)] = {
210 "metadata": metadata.duplicate(true),
211 "sprite": str(metadata.get(
"sprite", metadata.get(
"sprite_id",
"")))
213 var runtime_options:Dictionary = options.duplicate(true)
214 if not runtime_options.has(
"tile_size"):
216 return compile_grid_runtime(tile_rows, tile_definitions, objects, runtime_options)
220func runtime_tile_at_cell(runtime:Dictionary, cell:Vector2i, fallback:Dictionary = {}) -> Dictionary:
221 var by_cell:Dictionary = runtime.get(
"records_by_cell", {})
if runtime.get(
"records_by_cell", {})
is Dictionary
else {}
222 var record = by_cell.get(_cell_key(cell), fallback)
223 return record.duplicate(true)
if record
is Dictionary
else fallback.duplicate(true)
230func runtime_tile_ref_at_cell(runtime:Dictionary, cell:Vector2i) -> Dictionary:
231 var by_cell:Dictionary = runtime.get(
"records_by_cell", {})
if runtime.get(
"records_by_cell", {})
is Dictionary
else {}
232 var record = by_cell.get(_cell_key(cell), {})
233 return record
if record
is Dictionary
else {}
239func build_tile_fill_runs(records:Array, merge_fields:Array = [
"color"], options:Dictionary = {}) -> Array:
240 var include_empty:bool =
bool(options.get(
"include_empty", true))
241 var empty_symbols:Array = options.get(
"empty_symbols", [
" "])
if options.get(
"empty_symbols", [])
is Array
else [
" "]
243 var current:Dictionary = {}
244 var current_key:String =
""
245 var current_row:int = -999999
246 var current_end_x:int = -999999
248 if not item
is Dictionary:
250 var record:Dictionary = item
251 var symbol:String = str(record.get(
"symbol",
""))
252 if not include_empty
and empty_symbols.has(symbol):
254 var cell:Vector2i = record.get(
"cell", Vector2i.ZERO)
255 var key:String = _draw_record_merge_key(record, merge_fields)
256 var can_extend:bool =
not current.is_empty()
and cell.y == current_row
and cell.x == current_end_x
and key == current_key
258 var span:Vector2i = current.get(
"span", Vector2i.ONE)
260 current[
"span"] = span
261 current_end_x = cell.x + 1
263 if not current.is_empty():
264 runs.append(current.duplicate(true))
265 current = record.duplicate(true)
269 current_end_x = cell.x + 1
270 if not current.is_empty():
271 runs.append(current.duplicate(true))
276func get_visible_draw_records(records:Array, world_rect:Rect2, tile_pixel_size:Vector2 = Vector2.ZERO, options:Dictionary = {}) -> Array:
277 if world_rect.size.x <= 0.0
or world_rect.size.y <= 0.0:
278 return records.duplicate(true)
279 var pixel_size:Vector2 = tile_pixel_size
if tile_pixel_size != Vector2.ZERO
else Vector2(
float(tile_size.x),
float(tile_size.y))
280 var origin:Vector2 = _vector2(options.get(
"origin", Vector2.ZERO), Vector2.ZERO)
281 var margin:float =
float(options.get(
"margin", 0.0))
282 var visible_records:Array = []
283 for record
in records:
284 if not record
is Dictionary:
286 var rect:Rect2 = draw_record_world_rect(record, pixel_size, origin)
287 if rect_intersects_world_rect(rect, world_rect, margin):
288 visible_records.append(record)
289 return visible_records
293func draw_record_world_rect(record:Dictionary, tile_pixel_size:Vector2 = Vector2.ZERO, origin:Vector2 = Vector2.ZERO) -> Rect2:
294 if record.get(
"rect", null)
is Rect2:
295 return record.get(
"rect",
Rect2())
296 var pixel_size:Vector2 = tile_pixel_size
if tile_pixel_size != Vector2.ZERO
else Vector2(
float(tile_size.x),
float(tile_size.y))
297 var cell:Vector2i = record.get(
"cell", Vector2i.ZERO)
298 var span:Vector2i = record.get(
"span", Vector2i.ONE)
299 var size:Vector2 =
Vector2(pixel_size.x *
float(max(1, span.x)), pixel_size.y *
float(max(1, span.y)))
308func get_objects_in_rect(kind:String =
"", world_rect:Rect2 =
Rect2(), options:Dictionary = {}) -> Array:
309 var source_objects:Array = get_objects(kind)
310 if world_rect.size.x <= 0.0
or world_rect.size.y <= 0.0:
311 return source_objects
312 var margin:float =
float(options.get(
"margin", 0.0))
314 var object_size:Vector2 = _vector2(options.get(
"object_size", default_size))
315 var offset:Vector2 = _vector2(options.get(
"offset", Vector2.ZERO))
316 var visible_objects:Array = []
317 for object
in source_objects:
318 var rect:Rect2 = object_world_rect(object, object_size, offset)
319 if rect_intersects_world_rect(rect, world_rect, margin):
320 visible_objects.append(object)
321 return visible_objects
325func object_world_rect(object:Dictionary, fallback_size:Vector2 = Vector2.ZERO, offset:Vector2 = Vector2.ZERO) -> Rect2:
327 var size_value:Vector2 = _vector2(object.get(
"size", fallback_size
if fallback_size != Vector2.ZERO
else default_size))
328 var position:Vector2 = _object_world_position(object) + offset
329 return Rect2(position, size_value)
336func object_world_position(object:Dictionary, options:Dictionary = {}) -> Vector2:
338 var normalized:Dictionary = object.duplicate(true)
339 if options.has(
"position_units")
and not normalized.has(
"position_units"):
340 normalized[
"position_units"] = str(options.get(
"position_units",
"pixels"))
341 return _object_world_position_with_tile_size(normalized, size_value)
345func tile_units_to_world_position(value, tile_pixel_size:Vector2 = Vector2.ZERO) -> Vector2:
346 var size_value:Vector2 = tile_pixel_size
if tile_pixel_size != Vector2.ZERO
else Vector2(
float(tile_size.x),
float(tile_size.y))
347 return _vector2(value, Vector2.ZERO) * size_value
351func world_position_to_tile_units(position:Vector2, tile_pixel_size:Vector2 = Vector2.ZERO, digits:int = 3) -> Array:
352 var size_value:Vector2 = tile_pixel_size
if tile_pixel_size != Vector2.ZERO
else Vector2(
float(tile_size.x),
float(tile_size.y))
354 _round_float(position.x /
float(max(1.0, size_value.x)), digits),
355 _round_float(position.y /
float(max(1.0, size_value.y)), digits)
364func object_visual_rect(object:Dictionary, fallback_size:Vector2 = Vector2.ZERO, options:Dictionary = {}) -> Rect2:
366 var default_size:Vector2 = fallback_size
if fallback_size != Vector2.ZERO
else default_tile_size
367 var size_value:Vector2 = _vector2(object.get(
"visual_size", object.get(
"size", default_size)), default_size)
368 if str(object.get(
"visual_units", options.get(
"visual_units",
"pixels"))) ==
"tiles":
369 size_value *= default_tile_size
370 var anchor:Vector2 = _vector2(object.get(
"visual_anchor", options.get(
"visual_anchor",
Vector2(0.5, 0.5))),
Vector2(0.5, 0.5))
371 var visual_offset:Vector2 = _vector2(object.get(
"visual_offset", options.get(
"visual_offset", Vector2.ZERO)), Vector2.ZERO)
372 if str(object.get(
"visual_offset_units", options.get(
"visual_offset_units",
"pixels"))) ==
"tiles":
373 visual_offset *= default_tile_size
374 var placement:Vector2 = _object_visual_placement(object, default_tile_size, options)
375 return Rect2(placement - size_value * anchor + visual_offset, size_value)
382func build_object_draw_records(objects:Array, options:Dictionary = {}) -> Array:
384 var default_size:Vector2 = _vector2(options.get(
"fallback_size", default_tile_size), default_tile_size)
385 var records:Array = []
387 if not item
is Dictionary:
389 var object:Dictionary = item
390 var fallback_size:Vector2 = _vector2(object.get(
"_fallback_visual_size", default_size), default_size)
391 var visual_rect:Rect2 = object_visual_rect(object, fallback_size, options)
392 var center:Vector2 = _vector2(object.get(
"_world_center", visual_rect.get_center()), visual_rect.get_center())
394 "object": object.duplicate(true),
397 "kind": str(object.get(
"kind",
"")),
398 "sprite_id": str(object.get(
"sprite_id",
"")),
399 "category": str(object.get(
"category",
"object")),
400 "display_name": str(object.get(
"display_name", object.get(
"id", object.get(
"kind",
"Object")))),
401 "tags": object.get(
"tags", [])
if object.get(
"tags", [])
is Array
else []
407func rect_intersects_world_rect(rect:Rect2, world_rect:Rect2, margin:float = 0.0) -> bool:
408 var cull_rect:Rect2 = world_rect.grow(margin)
if margin != 0.0
else world_rect
409 return rect.intersects(cull_rect)
or cull_rect.has_point(rect.position)
412func _compile_tile_runtime_record(raw_record:Dictionary, runtime_tile_size:Vector2, surface_colors:Dictionary) -> Dictionary:
413 var record:Dictionary = raw_record.duplicate(true)
414 var tile:Dictionary = record.get(
"tile", {})
if record.get(
"tile", {})
is Dictionary
else {}
415 var metadata:Dictionary = record.get(
"metadata", {})
if record.get(
"metadata", {})
is Dictionary
else {}
416 var surface:String = str(metadata.get(
"surface", metadata.get(
"type",
"")))
417 var tile_type:String = str(metadata.get(
"type",
"empty" if surface ==
"void" else "floor"))
418 var cell:Vector2i = record.get(
"cell", Vector2i.ZERO)
419 record[
"type"] = tile_type
420 record[
"surface"] = surface
421 record[
"collision_shape"] = str(metadata.get(
"collision_shape",
""))
422 record[
"friction"] =
float(metadata.get(
"friction", 1.0))
423 record[
"bounce"] =
float(metadata.get(
"bounce", 0.82))
424 record[
"sprite_id"] = str(tile.get(
"sprite", metadata.get(
"sprite_id",
"")))
425 record[
"draw_grid"] =
bool(metadata.get(
"draw_grid", true))
and str(record.get(
"symbol",
"")) !=
" "
426 record[
"draw_sprite"] = str(record.get(
"symbol",
"")) !=
" "
427 record[
"rect"] =
Rect2(
Vector2(
float(cell.x) * runtime_tile_size.x,
float(cell.y) * runtime_tile_size.y), runtime_tile_size)
428 if metadata.has(
"color"):
429 record[
"color"] = _color_from_value(metadata.get(
"color",
"#ffffff"), Color.WHITE)
430 elif surface_colors.has(surface):
431 record[
"color"] = _color_from_value(surface_colors.get(surface), Color.WHITE)
432 elif surface_colors.has(tile_type):
433 record[
"color"] = _color_from_value(surface_colors.get(tile_type), Color.WHITE)
437func _cell_key(cell:Vector2i) -> String:
438 return "%d,%d" % [cell.x, cell.y]
441func _color_from_value(value, fallback:Color) -> Color:
444 var text:String = str(value)
450func _tile_records_for_include_empty(include_empty:bool) -> Array:
451 var cache_key =
"all" if include_empty
else "non_empty"
452 if tile_records_cache.has(cache_key):
454 return tile_records_cache.get(cache_key, [])
458 var layer_id = str(layer.get(
"id",
"terrain"))
459 var layer_tiles = layer.get(
"tiles", [])
460 for y
in range(layer_tiles.size()):
461 var row = str(layer_tiles[y])
462 for x
in range(row.length()):
464 var tile_metadata = get_tile_metadata(tile)
465 if not include_empty
and str(tile_metadata.get(
"type",
"")) ==
"empty":
471 "position":
Vector2(x * tile_size.x, y * tile_size.y),
472 "metadata": tile_metadata
474 tile_records_cache[cache_key] = records
478func _record_intersects_world_rect(record:Dictionary, world_rect:Rect2) -> bool:
479 var tile_rect =
Rect2(record.get(
"position", Vector2.ZERO),
Vector2(tile_size))
480 return rect_intersects_world_rect(tile_rect, world_rect)
484func get_objects(kind:String =
"") -> Array:
486 for object
in objects:
487 if not object
is Dictionary:
489 if kind !=
"" and str(object.get(
"kind",
"")) != kind:
491 results.append(object.duplicate(true))
496func get_spawns(kind:String =
"") -> Array:
498 for object
in get_objects(kind):
499 var entry = object.duplicate(true)
500 if entry.has(
"tile"):
501 var tile = entry[
"tile"]
502 if tile
is Array
and tile.size() >= 2:
503 entry[
"position"] =
Vector2(
float(tile[0]) * tile_size.x,
float(tile[1]) * tile_size.y)
508func get_exits() -> Array:
509 return get_objects(
"exit")
512func get_triggers(type:String =
"") -> Array:
514 for trigger
in triggers:
515 if not trigger
is Dictionary:
517 if type !=
"" and str(trigger.get(
"type",
"")) != type:
519 results.append(trigger.duplicate(true))
523func get_layers() -> Array:
524 return layers.duplicate(true)
527func get_level() -> Dictionary:
528 return level.duplicate(true)
532func get_cache_stats() -> Dictionary:
534 "tile_dimensions_cached": tile_dimensions_cache.x >= 0
and tile_dimensions_cache.y >= 0,
535 "rect_type_cache_entries": rects_by_type_cache.size(),
536 "tile_record_cache_entries": tile_records_cache.size(),
537 "cache_hits": cache_hits,
538 "cache_misses": cache_misses,
539 "data_cache_entries":
int(data_cache_manager.get_cache_stats().get(
"entries", 0)),
540 "data_cache_hits":
int(data_cache_manager.get_cache_stats().get(
"cache_hits", 0)),
541 "data_cache_misses":
int(data_cache_manager.get_cache_stats().get(
"cache_misses", 0))
546func clear_caches() -> void:
547 tile_dimensions_cache =
Vector2i(-1, -1)
548 rects_by_type_cache.clear()
549 tile_records_cache.clear()
555func get_background() -> Dictionary:
556 if level.has(
"background")
and level[
"background"]
is Dictionary:
557 return level[
"background"].duplicate(true)
558 return metadata.duplicate(true)
562func get_summary(data:Dictionary = {}) -> Dictionary:
563 var target = data
if not data.is_empty()
else level
564 var target_layers = _normalize_layers(target)
565 var target_objects = _normalize_objects(target.get(
"objects", []), target.get(
"spawns", []))
566 var dimensions := Vector2i.ZERO
567 for layer
in target_layers:
568 for row
in layer.get(
"tiles", []):
569 dimensions.x = max(dimensions.x, str(row).length())
570 dimensions.y = max(dimensions.y, layer.get(
"tiles", []).size())
572 "id": str(target.get(
"id",
"")),
573 "name": str(target.get(
"name",
"")),
574 "dimensions": dimensions,
575 "layers": target_layers.size(),
576 "objects": target_objects.size(),
577 "triggers": target.get(
"triggers", []).size()
if target.get(
"triggers", [])
is Array
else 0
582func validate_level(data:Dictionary = {}) -> Array:
583 var target = data
if not data.is_empty()
else level
585 if target.is_empty():
586 return [_issue(
"empty_level",
"level",
"No level data loaded.")]
587 var target_legend = target.get(
"legend", {})
588 if not target_legend
is Dictionary
or target_legend.is_empty():
589 issues.append(_issue(
"missing_legend",
"legend",
"Level must define a tile legend."))
590 var normalized_layers = _normalize_layers(target)
591 if normalized_layers.is_empty():
592 issues.append(_issue(
"missing_layers",
"layers",
"Level must define at least one tile layer or legacy tiles array."))
593 var seen_layer_ids := {}
594 var expected_width := -1
595 var expected_height := -1
596 for layer_index
in range(normalized_layers.size()):
597 var layer = normalized_layers[layer_index]
598 var layer_path =
"layers[%d]" % layer_index
599 var layer_id = str(layer.get(
"id",
""))
601 issues.append(_issue(
"missing_layer_id", layer_path,
"Layer is missing an id."))
602 elif seen_layer_ids.has(layer_id):
603 issues.append(_issue(
"duplicate_layer_id", layer_path,
"Duplicate layer id '%s'." % layer_id))
604 seen_layer_ids[layer_id] = true
605 var layer_tiles = layer.get(
"tiles", [])
606 if not layer_tiles
is Array
or layer_tiles.is_empty():
607 issues.append(_issue(
"empty_layer",
"%s.tiles" % layer_path,
"Layer must contain tile rows."))
609 if expected_height < 0:
610 expected_height = layer_tiles.size()
611 elif layer_tiles.size() != expected_height:
612 issues.append(_issue(
"layer_height_mismatch",
"%s.tiles" % layer_path,
"Layer height does not match the first layer."))
613 for y
in range(layer_tiles.size()):
614 var row = str(layer_tiles[y])
615 if expected_width < 0:
616 expected_width = row.length()
617 elif row.length() != expected_width:
618 issues.append(_issue(
"row_width_mismatch",
"%s.tiles[%d]" % [layer_path, y],
"Tile row width does not match the first row."))
619 for x
in range(row.length()):
621 if target_legend
is Dictionary
and not target_legend.has(tile):
622 issues.append(_issue(
"unknown_tile",
"%s.tiles[%d][%d]" % [layer_path, y, x],
"Tile '%s' is not in the legend." % tile))
623 var normalized_objects = _normalize_objects(target.get(
"objects", []), target.get(
"spawns", []))
624 var seen_object_ids := {}
625 var player_spawns := 0
626 for index
in range(normalized_objects.size()):
627 var object = normalized_objects[index]
628 var object_path =
"objects[%d]" % index
629 var object_id = str(object.get(
"id",
""))
631 if seen_object_ids.has(object_id):
632 issues.append(_issue(
"duplicate_object_id", object_path,
"Duplicate object id '%s'." % object_id))
633 seen_object_ids[object_id] = true
634 if str(object.get(
"kind",
"")) ==
"player":
636 var tile = object.get(
"tile", [])
637 if not _is_tile_array(tile):
638 issues.append(_issue(
"invalid_object_tile",
"%s.tile" % object_path,
"Object tile must be a two-number array."))
639 elif expected_width > 0
and expected_height > 0:
640 var cell = _cell_from_array(tile)
641 if cell.x < 0
or cell.y < 0
or cell.x >= expected_width
or cell.y >= expected_height:
642 issues.append(_issue(
"object_out_of_bounds",
"%s.tile" % object_path,
"Object is outside level bounds."))
643 if player_spawns <= 0:
644 issues.append(_issue(
"missing_player_spawn",
"objects",
"Level needs at least one player spawn."))
648func make_template_level(id:String =
"new_platformer_level", width:int = 24, height:int = 10) -> Dictionary:
649 var empty_row =
".".repeat(max(1, width))
650 var ground_row =
"#".repeat(max(1, width))
652 for y
in range(max(1, height)):
653 tile_rows.append(ground_row
if y == height - 1
else empty_row)
657 "name": id.replace(
"_",
" ").capitalize(),
658 "tile_size": [64, 64],
660 "game_type":
"platformer",
662 "music":
"prototype_loop",
666 ".": {
"type":
"empty"},
667 "#": {
"type":
"solid",
"sprite":
"grass_tile"},
668 "^": {
"type":
"hazard",
"sprite":
"spike_strip"},
669 "o": {
"type":
"pickup",
"sprite":
"coin_pickup"},
670 "F": {
"type":
"checkpoint",
"sprite":
"checkpoint_flag"}
673 {
"id":
"terrain",
"type":
"tiles",
"collision": true,
"visible": true,
"tiles": tile_rows}
676 {
"id":
"player_start",
"kind":
"player",
"tile": [2, max(0, height - 2)],
"facing":
"right"},
677 {
"id":
"exit_01",
"kind":
"exit",
"tile": [max(0, width - 2), max(0, height - 2)],
"target":
""}
690 tile_dimensions_cache =
Vector2i(-1, -1)
691 rects_by_type_cache.clear()
692 tile_records_cache.clear()
698func _normalize_layers(data:Dictionary) -> Array:
700 if data.get(
"layers", [])
is Array
and not data.get(
"layers", []).is_empty():
701 for raw_layer
in data.get(
"layers", []):
702 if not raw_layer
is Dictionary:
704 var layer = raw_layer.duplicate(true)
705 layer[
"id"] = str(layer.get(
"id",
"terrain"))
706 layer[
"type"] = str(layer.get(
"type",
"tiles"))
707 layer[
"collision"] =
bool(layer.get(
"collision", true))
708 layer[
"visible"] =
bool(layer.get(
"visible", true))
709 var layer_tiles := []
710 for row
in layer.get(
"tiles", []):
711 layer_tiles.append(str(row))
712 layer[
"tiles"] = layer_tiles
713 normalized.append(layer)
715 var legacy_tiles := []
716 for row
in data.get(
"tiles", []):
717 legacy_tiles.append(str(row))
718 if not legacy_tiles.is_empty():
724 "tiles": legacy_tiles
729func _normalize_objects(next_objects, legacy_spawns) -> Array:
731 if next_objects
is Array
and not next_objects.is_empty():
732 for raw_object
in next_objects:
733 if not raw_object
is Dictionary:
735 var object = raw_object.duplicate(true)
736 if not object.has(
"id")
or str(object.get(
"id",
"")) ==
"":
737 object[
"id"] =
"%s_%d" % [str(object.get(
"kind",
"object")), normalized.size()]
738 normalized.append(object)
740 if legacy_spawns
is Array:
741 for index
in range(legacy_spawns.size()):
742 var spawn = legacy_spawns[index]
743 if not spawn
is Dictionary:
745 var object = spawn.duplicate(true)
746 if not object.has(
"id")
or str(object.get(
"id",
"")) ==
"":
747 object[
"id"] =
"%s_%d" % [str(object.get(
"kind",
"spawn")), index]
748 normalized.append(object)
752func _get_layer(layer_id:String =
"") -> Dictionary:
753 if layers.is_empty():
756 return layers[0].duplicate(true)
758 if str(layer.get(
"id",
"")) == layer_id:
759 return layer.duplicate(true)
763func _parse_tile_size(value) -> Vector2i:
764 if value
is Array
and value.size() >= 2:
766 if value
is Vector2i:
773func _object_world_position(object:Dictionary) -> Vector2:
774 var position_value:Variant = object.get(
"position", null)
775 if position_value
is Vector2:
776 return position_value
777 if position_value
is Vector2i:
779 if position_value
is Array
and position_value.size() >= 2:
781 var tile_value:Variant = object.get(
"tile", [])
782 if tile_value
is Vector2i:
783 return Vector2(tile_value.x * tile_size.x, tile_value.y * tile_size.y)
784 if tile_value
is Vector2:
785 return Vector2(tile_value.x * tile_size.x, tile_value.y * tile_size.y)
786 if tile_value
is Array
and tile_value.size() >= 2:
787 return Vector2(
float(tile_value[0]) * tile_size.x,
float(tile_value[1]) * tile_size.y)
791func _object_visual_placement(object:Dictionary, default_tile_size:Vector2, options:Dictionary) -> Vector2:
792 var center_value:Variant = object.get(
"_world_center", null)
793 if center_value
is Vector2:
795 if center_value
is Vector2i:
797 if center_value
is Array
and center_value.size() >= 2:
799 var origin:Vector2 = _object_world_position_with_tile_size(object, default_tile_size)
800 var anchor_mode:String = str(object.get(
"placement_anchor", options.get(
"placement_anchor",
"center")))
801 if anchor_mode ==
"origin":
803 return origin + default_tile_size * 0.5
806func _object_world_position_with_tile_size(object:Dictionary, size_value:Vector2) -> Vector2:
807 var position_value:Variant = object.get(
"position", null)
808 if position_value
is Vector2:
809 return position_value
810 if position_value
is Vector2i:
812 if position_value
is Array
and position_value.size() >= 2:
813 var position_units:String = str(object.get(
"position_units",
"pixels"))
815 return parsed * size_value
if position_units ==
"tiles" else parsed
816 var tile_value:Variant = object.get(
"tile", [])
817 if tile_value
is Vector2i:
818 return Vector2(tile_value.x * size_value.x, tile_value.y * size_value.y)
819 if tile_value
is Vector2:
820 return Vector2(tile_value.x * size_value.x, tile_value.y * size_value.y)
821 if tile_value
is Array
and tile_value.size() >= 2:
822 return Vector2(
float(tile_value[0]) * size_value.x,
float(tile_value[1]) * size_value.y)
826func _vector2(value, fallback:Vector2 = Vector2.ZERO) -> Vector2:
829 if value
is Vector2i:
831 if value
is Array
and value.size() >= 2:
833 if value
is Dictionary:
834 return Vector2(
float(value.get(
"x", fallback.x)),
float(value.get(
"y", fallback.y)))
838func _draw_record_merge_key(record:Dictionary, merge_fields:Array) -> String:
840 for field_value
in merge_fields:
841 var field:String = str(field_value)
842 parts.append(
"%s=%s" % [field, str(record.get(field, null))])
843 return "|".join(parts)
846func _is_tile_array(value) -> bool:
847 return value
is Array
and value.size() >= 2
and (value[0]
is int
or value[0]
is float)
and (value[1]
is int
or value[1]
is float)
850func _cell_from_array(value:Array) -> Vector2i:
854func _round_float(value:float, digits:int = 3) -> float:
855 var factor:float = pow(10.0, digits)
856 return round(value * factor) / factor
859func _issue(type:String, path:String, message:String) -> Dictionary:
860 return {
"type": type,
"path": path,
"message": message}
Vector2
Return cached/static tile records whose world rect intersects a camera slice.
float
Test whether a world-space rect intersects a camera/rendering slice.