diff --git a/docs/src/workstreams.md b/docs/src/workstreams.md
index 13fc80b..f25a265 100644
--- a/docs/src/workstreams.md
+++ b/docs/src/workstreams.md
@@ -14,12 +14,13 @@ This section is a high level overview of what is being or can be worked on.
## Vehicle Physics Model
+- [ ] Kinematics low-speed model.
- [ ] Dynamics (force based) vehicle model.
- [ ] Implement rear-wheel drive.
- [ ] Implement four-wheel drive.
- [ ] Implement no-grip state (wheels have lost static grip on the road).
- [ ] Implement handbrake.
-- [ ] Kinematics low-speed model.
+ - [ ] Weight shifting
- [ ] Export parameters to be configurable in Godot editor.
## Audio
@@ -38,3 +39,7 @@ This section is a high level overview of what is being or can be worked on.
## Multiplayer
- [ ] ?
+
+## CI
+
+- [ ] Build and host book
diff --git a/godot/addons/ldtk-importer/README.md b/godot/addons/ldtk-importer/README.md
new file mode 100644
index 0000000..238e2b2
--- /dev/null
+++ b/godot/addons/ldtk-importer/README.md
@@ -0,0 +1,5 @@
+# ldtk-importer / godot-ldtk
+
+[LDtk](https://ldtk.io/) importer for [Godot 4](https://godotengine.org/)
+
+[Github](https://github.com/heygleeson/godot-ldtk)
diff --git a/godot/addons/ldtk-importer/ldtk-importer.gd b/godot/addons/ldtk-importer/ldtk-importer.gd
new file mode 100644
index 0000000..9682c43
--- /dev/null
+++ b/godot/addons/ldtk-importer/ldtk-importer.gd
@@ -0,0 +1,311 @@
+@tool
+extends EditorImportPlugin
+
+const LDTK_LATEST_VERSION = "1.5.3"
+
+enum Presets {DEFAULT}
+
+const Util = preload("src/util/util.gd")
+const World = preload("src/world.gd")
+const Level = preload("src/level.gd")
+const Tileset = preload("src/tileset.gd")
+const DefinitionUtil = preload("src/util/definition_util.gd")
+
+#region EditorImportPlugin Overrides
+
+#region Simple
+func _get_importer_name():
+ return "ldtk.import"
+
+func _get_visible_name():
+ return "LDTK Scene"
+
+func _get_priority():
+ return 1.0
+
+func _get_import_order():
+ return IMPORT_ORDER_SCENE
+
+func _get_resource_type():
+ return "PackedScene"
+
+func _get_recognized_extensions():
+ return ["ldtk"]
+
+func _get_save_extension():
+ return "scn"
+
+func _get_preset_count():
+ return Presets.size()
+
+func _get_preset_name(index):
+ match index:
+ Presets.DEFAULT:
+ return "Default"
+ _:
+ return "Unknown"
+
+func _get_option_visibility(path, option_name, options):
+ match option_name:
+ _:
+ return true
+ return true
+
+func _can_import_threaded() -> bool:
+ return false
+
+#endregion
+
+func _get_import_options(path, index):
+ return [
+ # --- World --- #
+ {"name": "World", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Group LDTKLevels in 'LDTKWorldLayer' nodes if using LDTK's WorldDepth.
+ "name": "group_world_layers",
+ "default_value": false,
+ },
+ # --- Levels --- #
+ {"name": "Level", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Save LDTKLevels as PackedScenes.
+ "name": "pack_levels",
+ "default_value": true,
+ },
+ # --- Layers --- #
+ {"name": "Layer", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Save LDTKLevels as PackedScenes.
+ "name": "layers_always_visible",
+ "default_value": false,
+ },
+ # --- Tileset --- #
+ {"name": "Tileset", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Add LDTK Custom Data to Tilesets
+ "name": "tileset_custom_data",
+ "default_value": false,
+ },
+ {
+ # Create TileAtlasSources & TileMapLayers for IntGrid Layers
+ "name": "integer_grid_tilesets",
+ "default_value": false,
+ },
+ {
+ # Define default texture type for TilesetAtlasSource (e.g. to apply normal maps to tilesets after import)
+ "name": "atlas_texture_type",
+ "default_value": 0,
+ "property_hint": PROPERTY_HINT_ENUM,
+ "hint_string": "CompressedTexture2D,CanvasTexture",
+ },
+ # --- Entities --- #
+ {"name": "Entity", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ #
+ "name": "resolve_entityrefs",
+ "default_value": true,
+ },
+ {
+ # Create LDTKEntityPlaceholder nodes to help debug importing.
+ "name": "use_entity_placeholders",
+ "default_value": false,
+ },
+ # --- Post Import --- #
+ {"name": "Post Import", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Define a post-import script to apply on imported Tilesets.
+ "name": "tileset_post_import",
+ "default_value": "",
+ "property_hint": PROPERTY_HINT_FILE,
+ "hint_string": "*.gd;GDScript"
+ },
+ {
+ # Define a post-import script to apply on imported Entities.
+ "name": "entities_post_import",
+ "default_value": "",
+ "property_hint": PROPERTY_HINT_FILE,
+ "hint_string": "*.gd;GDScript"
+ },
+ {
+ # Define a post-import script to apply on imported Levels.
+ "name": "level_post_import",
+ "default_value": "",
+ "property_hint": PROPERTY_HINT_FILE,
+ "hint_string": "*.gd;GDScript"
+ },
+ {
+ # Define a post-import script to apply on imported Worlds.
+ "name": "world_post_import",
+ "default_value": "",
+ "property_hint": PROPERTY_HINT_FILE,
+ "hint_string": "*.gd;GDScript"
+ },
+ # --- Debug --- #
+ {"name": "Debug", "default_value":"", "usage": PROPERTY_USAGE_GROUP},
+ {
+ # Force Tilesets to be recreated, resetting modifications (if experiencing import issues)
+ "name": "force_tileset_reimport",
+ "default_value": false,
+ },
+ {
+ # Debug: Enable Verbose Output (used by the importer)
+ "name": "verbose_output", "default_value": false
+ }
+ ]
+
+func _import(
+ source_file: String,
+ save_path: String,
+ options: Dictionary,
+ platform_variants: Array[String],
+ gen_files: Array[String]
+) -> Error:
+
+ Util.timer_reset()
+ Util.timer_start(Util.DebugTime.TOTAL)
+ Util.print("import_start", source_file)
+
+ # Add options to static var in "Util", accessible from any script.
+ Util.options = options
+
+ # Parse source_file
+ var base_dir := source_file.get_base_dir() + "/"
+ var file_name := source_file.get_file()
+ var world_name := file_name.split(".")[0]
+
+ Util.timer_start(Util.DebugTime.LOAD)
+ var world_data := Util.parse_file(source_file)
+ Util.timer_finish("File parsed")
+
+ # Check version
+ if Util.check_version(world_data.jsonVersion, LDTK_LATEST_VERSION):
+ Util.print("item_ok", "LDTK VERSION (%s) OK" % [world_data.jsonVersion])
+ else:
+ return ERR_PARSE_ERROR
+
+ Util.timer_start(Util.DebugTime.GENERAL)
+ var definitions := DefinitionUtil.build_definitions(world_data)
+ var tileset_overrides := Tileset.get_tileset_overrides(world_data)
+ Util.timer_finish("Definitions Created")
+
+ # Build Tilesets and save as Resources
+ if Util.options.verbose_output: Util.print("block", "Tilesets")
+ var tileset_paths := Tileset.build_tilesets(definitions, base_dir, tileset_overrides)
+ gen_files.append_array(tileset_paths)
+
+ # Fetch EntityDef Tile textures
+ Tileset.get_entity_def_tiles(definitions, Util.tilesets)
+
+ # Detect Multi-Worlds
+ var external_levels: bool = world_data.externalLevels
+ var world_iid: String = world_data.iid
+
+ var world: LDTKWorld
+ if world_data.worldLayout == null:
+ var world_nodes: Array[LDTKWorld] = []
+ var world_instances: Array = world_data.worlds
+ # Build each world instance
+ for world_instance in world_instances:
+ var world_instance_name: String = world_instance.identifier
+ var world_instance_iid: String = world_instance.iid
+ var levels := Level.build_levels(world_instance, definitions, base_dir, external_levels)
+ var world_node := World.create_world(world_instance_name, world_instance_iid, levels, base_dir)
+ world_nodes.append(world_node)
+
+ world = World.create_multi_world(world_name, world_iid, world_nodes)
+ else:
+ if Util.options.verbose_output: Util.print("block", "Levels")
+ var levels := Level.build_levels(world_data, definitions, base_dir, external_levels)
+
+ # Save Levels (after Level Post-Import)
+ if (Util.options.pack_levels):
+ var levels_path := base_dir + 'levels/'
+ var directory = DirAccess.open(base_dir)
+ if not directory.dir_exists(levels_path):
+ directory.make_dir(levels_path)
+
+ # Resolve Refs + Cleanup Resolvers. We don't want to save 'NodePathResolver' in the Level scene.
+ #if (Util.options.verbose_output): Util.print("block", "References")
+ if (Util.options.verbose_output): Util.print("block", "Save Levels")
+ Util.handle_references()
+ var packed_levels = save_levels(levels, levels_path, gen_files)
+
+ if (Util.options.verbose_output): Util.print("block", "Save World")
+ world = World.create_world(world_name, world_iid, packed_levels, base_dir)
+ else:
+ if (Util.options.verbose_output): Util.print("block", "Save World")
+ world = World.create_world(world_name, world_iid, levels, base_dir)
+
+ Util.handle_references()
+
+ # Save World as PackedScene
+ Util.timer_start(Util.DebugTime.SAVE)
+ var err = save_world(world, save_path, gen_files)
+ Util.timer_finish("World Saved", 1)
+
+ if Util.options.verbose_output: Util.print("block", "Results")
+
+ Util.timer_finish("Completed.")
+
+ var total_time: int = Util.DebugTime.get_total_time()
+ var result_message: String = Util.DebugTime.get_result()
+
+ if Util.options.verbose_output: Util.print("item_info", result_message)
+ Util.print("import_finish", str(total_time))
+
+ return err
+
+#endregion
+
+func save_world(
+ world: LDTKWorld,
+ save_path: String,
+ gen_files: Array[String]
+) -> Error:
+ var packed_world = PackedScene.new()
+ packed_world.pack(world)
+
+ Util.print("item_save", "Saving World [color=#fe8019][i]'%s'[/i][/color]" % [save_path], 1)
+
+ var world_path = "%s.%s" % [save_path, _get_save_extension()]
+ var err = ResourceSaver.save(packed_world, world_path)
+ if err == OK:
+ gen_files.append(world_path)
+ return err
+
+func save_levels(
+ levels: Array[LDTKLevel],
+ save_path: String,
+ gen_files: Array[String]
+) -> Array[LDTKLevel]:
+ Util.timer_start(Util.DebugTime.SAVE)
+ var packed_levels: Array[LDTKLevel] = []
+
+
+ var level_names := levels.map(func(elem): return elem.name)
+ Util.print("item_save", "Saving Levels: [color=#fe8019]%s[/color]" % [level_names], 1)
+
+ for level in levels:
+ for child in level.get_children():
+ Util.recursive_set_owner(child, level)
+ var level_path = save_level(level, save_path, gen_files)
+ var packed_level = load(level_path).instantiate()
+ packed_levels.append(packed_level)
+
+ Util.timer_finish("%s Levels Saved" % [levels.size()], 1)
+ return packed_levels
+
+func save_level(
+ level: LDTKLevel,
+ save_path: String,
+ gen_files: Array[String]
+) -> String:
+ var packed_level = PackedScene.new()
+ packed_level.pack(level)
+ var level_path = "%s%s.%s" % [save_path, level.name, _get_save_extension()]
+
+ var err = ResourceSaver.save(packed_level, level_path)
+ if err == OK:
+ gen_files.append(level_path)
+
+ return level_path
diff --git a/godot/addons/ldtk-importer/ldtk-importer.gd.uid b/godot/addons/ldtk-importer/ldtk-importer.gd.uid
new file mode 100644
index 0000000..8ae16f1
--- /dev/null
+++ b/godot/addons/ldtk-importer/ldtk-importer.gd.uid
@@ -0,0 +1 @@
+uid://lnqlil4dt4ca
diff --git a/godot/addons/ldtk-importer/plugin.cfg b/godot/addons/ldtk-importer/plugin.cfg
new file mode 100644
index 0000000..53c609d
--- /dev/null
+++ b/godot/addons/ldtk-importer/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="LDTK"
+description="LDtk Import plugin for Godot 4!"
+author="Andy Gleeson (gleeson.dev)"
+version="2.0.1"
+script="plugin.gd"
diff --git a/godot/addons/ldtk-importer/plugin.gd b/godot/addons/ldtk-importer/plugin.gd
new file mode 100644
index 0000000..3681af8
--- /dev/null
+++ b/godot/addons/ldtk-importer/plugin.gd
@@ -0,0 +1,19 @@
+@tool
+extends EditorPlugin
+
+var ldtk_plugin
+var config = ConfigFile.new()
+
+func _enter_tree() -> void:
+ ldtk_plugin = preload("ldtk-importer.gd").new()
+ add_import_plugin(ldtk_plugin)
+
+ var config = ConfigFile.new()
+ var err = config.load("res://addons/ldtk-importer/plugin.cfg")
+ var version = config.get_value("plugin", "version", "0.0")
+
+ print_rich("[color=#ffcc00]█ Godot-LDtk-Importer █[/color] %s | [url=https://gleeson.dev]@gleeson.dev[/url] | [url=https://github.com/heygleeson/godot-ldtk-importer]View on Github[/url]" % [version])
+
+func _exit_tree() -> void:
+ remove_import_plugin(ldtk_plugin)
+ ldtk_plugin = null
diff --git a/godot/addons/ldtk-importer/plugin.gd.uid b/godot/addons/ldtk-importer/plugin.gd.uid
new file mode 100644
index 0000000..3cf8dd5
--- /dev/null
+++ b/godot/addons/ldtk-importer/plugin.gd.uid
@@ -0,0 +1 @@
+uid://bqk4o57wsdal2
diff --git a/godot/addons/ldtk-importer/post-import/entity-template.gd b/godot/addons/ldtk-importer/post-import/entity-template.gd
new file mode 100644
index 0000000..b95b323
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/entity-template.gd
@@ -0,0 +1,15 @@
+@tool
+
+# Entity Post-Import Template for LDTK-Importer.
+
+func post_import(entity_layer: LDTKEntityLayer) -> LDTKEntityLayer:
+ var definition: Dictionary = entity_layer.definition
+ var entities: Array = entity_layer.entities
+
+ #print("EntityLayer: ", entity_layer.name, " | Count: ", entities.size())
+
+ for entity in entities:
+ # Perform operations here
+ pass
+
+ return entity_layer
diff --git a/godot/addons/ldtk-importer/post-import/entity-template.gd.uid b/godot/addons/ldtk-importer/post-import/entity-template.gd.uid
new file mode 100644
index 0000000..b36c9bf
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/entity-template.gd.uid
@@ -0,0 +1 @@
+uid://c56fiui27y4ww
diff --git a/godot/addons/ldtk-importer/post-import/level-template.gd b/godot/addons/ldtk-importer/post-import/level-template.gd
new file mode 100644
index 0000000..5dc18df
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/level-template.gd
@@ -0,0 +1,8 @@
+@tool
+
+# Level Post-Import Template for LDTK-Importer.
+
+func post_import(level: LDTKLevel) -> LDTKLevel:
+ # Behaviour goes here
+ #print("Level: ", level)
+ return level
diff --git a/godot/addons/ldtk-importer/post-import/level-template.gd.uid b/godot/addons/ldtk-importer/post-import/level-template.gd.uid
new file mode 100644
index 0000000..f76267e
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/level-template.gd.uid
@@ -0,0 +1 @@
+uid://05p0rgy0kklx
diff --git a/godot/addons/ldtk-importer/post-import/tileset-template.gd b/godot/addons/ldtk-importer/post-import/tileset-template.gd
new file mode 100644
index 0000000..87b4b3a
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/tileset-template.gd
@@ -0,0 +1,10 @@
+@tool
+
+# Tileset Post-Import Template for LDTK-Importer.
+
+func post_import(tilesets: Dictionary) -> Dictionary:
+ # Behaviour goes here
+ for tileset: TileSet in tilesets.values():
+ #print("Tileset: ", tileset, tileset.tile_size)
+ pass
+ return tilesets
diff --git a/godot/addons/ldtk-importer/post-import/tileset-template.gd.uid b/godot/addons/ldtk-importer/post-import/tileset-template.gd.uid
new file mode 100644
index 0000000..60305d6
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/tileset-template.gd.uid
@@ -0,0 +1 @@
+uid://bwex3re84r6o2
diff --git a/godot/addons/ldtk-importer/post-import/world-template.gd b/godot/addons/ldtk-importer/post-import/world-template.gd
new file mode 100644
index 0000000..94e6337
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/world-template.gd
@@ -0,0 +1,8 @@
+@tool
+
+# World Post-Import Template for LDTK-Importer.
+
+func post_import(world: LDTKWorld) -> LDTKWorld:
+ # Behaviour goes here
+ #print("World: ", world)
+ return world
diff --git a/godot/addons/ldtk-importer/post-import/world-template.gd.uid b/godot/addons/ldtk-importer/post-import/world-template.gd.uid
new file mode 100644
index 0000000..6db12ef
--- /dev/null
+++ b/godot/addons/ldtk-importer/post-import/world-template.gd.uid
@@ -0,0 +1 @@
+uid://bko7ojwy2jnl
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd
new file mode 100644
index 0000000..ec48f9a
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd
@@ -0,0 +1,8 @@
+@tool
+@icon("ldtk-entity-layer.svg")
+class_name LDTKEntityLayer
+extends Node2D
+
+@export var iid: String
+@export var definition: Dictionary
+@export var entities: Array
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd.uid b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd.uid
new file mode 100644
index 0000000..cf37932
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd.uid
@@ -0,0 +1 @@
+uid://c6umqvu1pttjk
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg
new file mode 100644
index 0000000..3d2f9da
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg.import b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg.import
new file mode 100644
index 0000000..b60e310
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cjhytibfjjb2y"
+path="res://.godot/imported/ldtk-entity-layer.svg-4429574458c1abbb5905ae9e6174fef3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ldtk-importer/src/components/ldtk-entity-layer.svg"
+dest_files=["res://.godot/imported/ldtk-entity-layer.svg-4429574458c1abbb5905ae9e6174fef3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.tscn b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.tscn
new file mode 100644
index 0000000..a658482
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity-layer.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://dndtl8sgpdo6m"]
+
+[ext_resource type="Script" path="res://addons/ldtk-importer/src/components/ldtk-entity-layer.gd" id="1_kdfje"]
+
+[node name="LDTK Entity Layer" type="Node2D"]
+script = ExtResource("1_kdfje")
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity.gd b/godot/addons/ldtk-importer/src/components/ldtk-entity.gd
new file mode 100644
index 0000000..b95f575
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity.gd
@@ -0,0 +1,96 @@
+@icon("ldtk-entity.svg")
+@tool
+class_name LDTKEntity
+extends Node2D
+
+## Placeholder Node for importing LDTK maps.
+## Used to demonstrate import functionality - please write an Entity Post-Import script to spawn
+## your own instances when using this in your project.
+
+@export var iid: String
+@export var identifier := "EntityPlaceholder"
+@export var fields := {}
+@export var pivot := Vector2.ZERO
+@export var size := Vector2.ZERO
+@export var smart_color := Color.hex(0xffcc0088)
+@export var definition := {}
+
+@onready var sprite: Sprite2D = $Sprite
+
+var _refs := []
+var _points := []
+var _drawPaths := false
+
+func _ready() -> void:
+ _points.append(Vector2.ZERO)
+ for key in fields:
+ if fields[key] is NodePath:
+ _refs.append(fields[key])
+ elif fields[key] is Vector2i:
+ _points.append(fields[key])
+ elif fields[key] is Array:
+ for value in fields[key]:
+ if value is NodePath:
+ _refs.append(value)
+ elif value is Vector2i:
+ _points.append(value)
+ else:
+ break
+ _drawPaths = _refs.size() > 0 or _points.size() > 0
+ _points = _parse_points(_points)
+ queue_redraw()
+
+func _draw() -> void:
+ if definition.is_empty():
+ return
+
+ match definition.renderMode:
+ "Ellipse":
+ if definition.hollow:
+ draw_arc((size * 0.5) + size * -pivot, size.x * 0.5, 0, TAU, 24, smart_color, 1.0)
+ else:
+ draw_circle((size * 0.5) + size * -pivot, size.x * 0.5, smart_color)
+ "Rectangle":
+ if definition.hollow:
+ draw_rect(Rect2(size * -pivot, size), smart_color, false, 1.0)
+ else:
+ draw_rect(Rect2(size * -pivot, size), smart_color, true)
+ "Cross":
+ draw_line(Vector2.ZERO, size, smart_color, 3.0)
+ draw_line(Vector2(0, size.y), Vector2(size.x, 0), smart_color, 3.0)
+ "Tile":
+ if definition.tile == null:
+ pass
+ if definition.tile is Texture2D:
+ sprite.texture = definition.tile
+
+ if _drawPaths:
+ for path in _refs:
+ if path is not NodePath:
+ continue
+ elif path.is_empty():
+ continue
+ var node = get_node(path)
+ if node != null:
+ draw_dashed_line(Vector2.ZERO, node.global_position - global_position, smart_color)
+
+ var previousPoint = _points[0]
+ for point in _points:
+ if point == previousPoint:
+ continue
+ draw_dashed_line(Vector2(previousPoint), Vector2(point), smart_color, 1.0, 4.0)
+ draw_arc(point, 4.0, 0, TAU, 5, smart_color, 1.0)
+ previousPoint = point
+
+func _parse_points(points: Array) -> Array:
+ if points.size() == 0:
+ return points
+ if get_parent() is SubViewport:
+ return points
+ var origin = get_parent().global_position - global_position
+ var gridSize = get_parent().definition.gridSize
+ var cellOffset = gridSize * Vector2(0.5, 0.5)
+ for index in range(1, points.size()):
+ var pixelCoord = points[index] * gridSize
+ points[index] = origin + pixelCoord + cellOffset
+ return points
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity.gd.uid b/godot/addons/ldtk-importer/src/components/ldtk-entity.gd.uid
new file mode 100644
index 0000000..951d5d6
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity.gd.uid
@@ -0,0 +1 @@
+uid://ciskd8jyty7gq
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity.svg b/godot/addons/ldtk-importer/src/components/ldtk-entity.svg
new file mode 100644
index 0000000..3396243
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity.svg.import b/godot/addons/ldtk-importer/src/components/ldtk-entity.svg.import
new file mode 100644
index 0000000..91f015e
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c8jokgcqymy8i"
+path="res://.godot/imported/ldtk-entity.svg-0e9e158ef8dc55d8602ffe476bdddfa3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ldtk-importer/src/components/ldtk-entity.svg"
+dest_files=["res://.godot/imported/ldtk-entity.svg-0e9e158ef8dc55d8602ffe476bdddfa3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-entity.tscn b/godot/addons/ldtk-importer/src/components/ldtk-entity.tscn
new file mode 100644
index 0000000..b48b4e6
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-entity.tscn
@@ -0,0 +1,9 @@
+[gd_scene load_steps=2 format=3 uid="uid://c36l6fpttus66"]
+
+[ext_resource type="Script" path="res://addons/ldtk-importer/src/components/ldtk-entity.gd" id="1_hclg8"]
+
+[node name="LDTK Entity" type="Node2D"]
+script = ExtResource("1_hclg8")
+smart_color = Color(1, 0.8, 0, 0.533333)
+
+[node name="Sprite" type="Sprite2D" parent="."]
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-level.gd b/godot/addons/ldtk-importer/src/components/ldtk-level.gd
new file mode 100644
index 0000000..a1a98e7
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-level.gd
@@ -0,0 +1,18 @@
+@icon("ldtk-level.svg")
+@tool
+class_name LDTKLevel
+extends Node2D
+
+@export var iid: String
+@export var world_position: Vector2
+@export var size: Vector2i
+@export var fields: Dictionary
+@export var neighbours: Array
+@export var bg_color: Color
+
+func _ready() -> void:
+ queue_redraw()
+
+func _draw() -> void:
+ if Engine.is_editor_hint():
+ draw_rect(Rect2(Vector2.ZERO, size), bg_color, false, 2.0)
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-level.gd.uid b/godot/addons/ldtk-importer/src/components/ldtk-level.gd.uid
new file mode 100644
index 0000000..53ad847
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-level.gd.uid
@@ -0,0 +1 @@
+uid://devv1ueptyklo
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-level.svg b/godot/addons/ldtk-importer/src/components/ldtk-level.svg
new file mode 100644
index 0000000..00a76fa
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-level.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-level.svg.import b/godot/addons/ldtk-importer/src/components/ldtk-level.svg.import
new file mode 100644
index 0000000..d4a98b6
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-level.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bn8846b01v84"
+path="res://.godot/imported/ldtk-level.svg-7c4c13bcdc12c18fc0e3338deb18be11.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ldtk-importer/src/components/ldtk-level.svg"
+dest_files=["res://.godot/imported/ldtk-level.svg-7c4c13bcdc12c18fc0e3338deb18be11.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-level.tscn b/godot/addons/ldtk-importer/src/components/ldtk-level.tscn
new file mode 100644
index 0000000..465fbfe
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-level.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://5jp4st4vnouy"]
+
+[ext_resource type="Script" path="res://addons/ldtk-importer/src/components/ldtk-level.gd" id="1_4fbyi"]
+
+[node name="LDTK Level" type="Node2D"]
+script = ExtResource("1_4fbyi")
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd
new file mode 100644
index 0000000..b04db2c
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd
@@ -0,0 +1,9 @@
+@tool
+@icon("ldtk-entity-layer.svg")
+class_name LDTKWorldLayer
+extends Node2D
+
+@export var depth: int:
+ set(d):
+ depth = d
+ z_index = depth
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd.uid b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd.uid
new file mode 100644
index 0000000..645d9b4
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd.uid
@@ -0,0 +1 @@
+uid://cvvkp8akfp51o
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world-layer.tscn b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.tscn
new file mode 100644
index 0000000..ed54260
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world-layer.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://tiadjdf7bmjd"]
+
+[ext_resource type="Script" path="res://addons/ldtk-importer/src/components/ldtk-world-layer.gd" id="1_tla3i"]
+
+[node name="World Layer" type="Node2D"]
+script = ExtResource("1_tla3i")
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world.gd b/godot/addons/ldtk-importer/src/components/ldtk-world.gd
new file mode 100644
index 0000000..73335b1
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world.gd
@@ -0,0 +1,22 @@
+@tool
+@icon("ldtk-world.svg")
+class_name LDTKWorld
+extends Node2D
+
+@export var iid: String
+@export var rect: Rect2i
+@export var levels: Array[LDTKLevel]
+
+func _init() -> void:
+ child_order_changed.connect(_find_level_children)
+
+func _find_level_children() -> void:
+ for child in get_children():
+ if child is LDTKLevel:
+ if not levels.has(child):
+ levels.append(child)
+ else:
+ for grandchild in child.get_children():
+ if grandchild is LDTKLevel:
+ if not levels.has(grandchild):
+ levels.append(grandchild)
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world.gd.uid b/godot/addons/ldtk-importer/src/components/ldtk-world.gd.uid
new file mode 100644
index 0000000..84f87c4
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world.gd.uid
@@ -0,0 +1 @@
+uid://boyf0tvvercyb
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world.svg b/godot/addons/ldtk-importer/src/components/ldtk-world.svg
new file mode 100644
index 0000000..975c0a7
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world.svg.import b/godot/addons/ldtk-importer/src/components/ldtk-world.svg.import
new file mode 100644
index 0000000..6c924be
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bhwcp2d8b7ofe"
+path="res://.godot/imported/ldtk-world.svg-afb7a8fc57a903b71679a614011495e8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ldtk-importer/src/components/ldtk-world.svg"
+dest_files=["res://.godot/imported/ldtk-world.svg-afb7a8fc57a903b71679a614011495e8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/godot/addons/ldtk-importer/src/components/ldtk-world.tscn b/godot/addons/ldtk-importer/src/components/ldtk-world.tscn
new file mode 100644
index 0000000..7e65778
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/components/ldtk-world.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://b5smuv4vq5lsp"]
+
+[ext_resource type="Script" path="res://addons/ldtk-importer/src/components/ldtk-world.gd" id="1_k4v0p"]
+
+[node name="LDTK World" type="Node2D"]
+script = ExtResource("1_k4v0p")
diff --git a/godot/addons/ldtk-importer/src/layer.gd b/godot/addons/ldtk-importer/src/layer.gd
new file mode 100644
index 0000000..e42bb92
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/layer.gd
@@ -0,0 +1,291 @@
+@tool
+
+const Util = preload("util/util.gd")
+const LayerUtil = preload("util/layer-util.gd")
+const FieldUtil = preload("util/field-util.gd")
+const TileUtil = preload("util/tile-util.gd")
+
+# Used to display helpful error messages (which level we are in)
+static var current_level: String = "N/A"
+
+static func create_layers(
+ level_data: Dictionary,
+ layer_instances: Array,
+ definitions: Dictionary
+) -> Array:
+
+ current_level = level_data.identifier
+
+ var layer_nodes := []
+ var layer_index: int = 0
+
+ for layer_instance in layer_instances:
+ var layer_def: Dictionary = definitions.layers[layer_instance.layerDefUid]
+ var layer_type: String = layer_instance.__type
+
+ match layer_type:
+ "Entities":
+ var layer = create_entity_layer(layer_instance, layer_def, definitions.entities)
+ layer_nodes.push_front(layer)
+
+ "IntGrid":
+ # Determines if this has an 'AutoLayer' assigned
+ var has_tileset := layer_instance.__tilesetDefUid != null
+
+ if has_tileset:
+ var layer = create_tile_layer(layer_instance, layer_def)
+ layer_nodes.push_front(layer)
+ if (not has_tileset or Util.options.integer_grid_tilesets):
+ var layer = create_intgrid_layer(layer_instance, layer_def)
+ layer_nodes.push_front(layer)
+
+ "Tiles", "AutoLayer":
+ var layer = create_tile_layer(layer_instance, layer_def)
+ layer_nodes.push_front(layer)
+
+ _:
+ push_warning("[LDtk] Tried importing an unsupported layer type: ", layer_type)
+
+ if (Util.options.verbose_output):
+ var layer_names = layer_nodes.map(func(elem): return elem.name)
+ Util.print("item_info", "Created Layers: [color=slategray]%s[/color]" % [layer_names], 2)
+ return layer_nodes
+
+#region Layer Types
+
+static func create_entity_layer(
+ layer_data: Dictionary,
+ layer_def: Dictionary,
+ entity_defs: Dictionary
+) -> LDTKEntityLayer:
+
+ var layer = LDTKEntityLayer.new()
+ layer.name = layer_data.__identifier
+ layer.iid = layer_data.iid
+ layer.position = layer_def.offset
+
+ # Create a dummy child node so EntityRef fields get a correct NodePath
+ # I need to find a better way to do this, but there are lots of funny behaviours to deal with.
+ var pathResolver = Node2D.new()
+ pathResolver.name = "NodePathResolver"
+ layer.add_child(pathResolver)
+ Util.path_resolvers.append(pathResolver)
+
+ var entities: Array = LayerUtil.parse_entity_instances(
+ layer_data.entityInstances,
+ entity_defs,
+ pathResolver
+ )
+
+ # Add instance references
+ layer.definition = layer_def
+ layer.entities = entities
+
+ if (Util.options.use_entity_placeholders):
+ LayerUtil.placeholder_counts.clear()
+ for entity in entities:
+ var placeholder: LDTKEntity = LayerUtil.create_entity_placeholder(layer, entity)
+ Util.update_instance_reference(placeholder.iid, placeholder)
+ #try_push_placeholder_ref(placeholder, pathResolver)
+ try_push_placeholder_ref(placeholder, placeholder)
+
+ return layer
+
+static func create_intgrid_layer(
+ layer_data: Dictionary,
+ layer_def: Dictionary
+) -> TileMapLayer:
+
+ # Create TileMapLayer
+ var tilemap: TileMapLayer = LayerUtil.create_layer_tilemap(layer_data)
+ tilemap.position = layer_def.offset
+
+ # Retrieve IntGrid values - these do not always match their array index
+ var values: Array = layer_def.intGridValues.map(
+ func(item): return item.value
+ )
+
+ # Set layer properties on the Tilemap
+ var layer_name := str(layer_data.__identifier) + "-values"
+ tilemap.set_name(layer_name)
+ tilemap.set_modulate(Color(1, 1, 1, layer_data.__opacity))
+
+ # Get tile data
+ var tiles: Array = layer_data.intGridCsv
+ var tile_source_id: int = layer_data.layerDefUid
+ var columns: int = layer_data.__cWid
+
+ # Place IntGrid value tiles
+ for index in range(0, tiles.size()):
+ var value = tiles[index]
+ var value_index: int = values.find(value)
+ if value_index != -1:
+ var cell_coords := TileUtil.index_to_grid(index, columns)
+ var tile_coords := Vector2i(value_index, 0)
+ tilemap.set_cell(cell_coords, tile_source_id, tile_coords)
+
+ return tilemap
+
+static func create_tile_layer(
+ layer_data: Dictionary,
+ layer_def: Dictionary
+) -> TileMapLayer:
+
+ # Create Tilemap
+ var tilemap: TileMapLayer = LayerUtil.create_layer_tilemap(layer_data)
+ tilemap.position = layer_def.offset
+
+ # Set layer properties on the Tilemap
+ var layer_name := str(layer_data.__identifier)
+ tilemap.set_name(layer_name)
+ tilemap.set_modulate(Color(1, 1, 1, layer_data.__opacity))
+
+ if not Util.options.layers_always_visible:
+ tilemap.set_enabled(layer_data.visible)
+
+ # Get tile data
+ var tiles: Array
+ if (layer_data.__type == "Tiles"):
+ tiles = layer_data.gridTiles
+ else:
+ tiles = layer_data.autoLayerTiles
+
+ var tile_source_id = layer_data.__tilesetDefUid
+ var grid_size := Vector2(layer_data.__gridSize, layer_data.__gridSize)
+
+ if (tile_source_id == null):
+ Util.print("item_fail", "Null Tilemap on layerInstance: '%s'" % [layer_name], 2)
+ push_warning("Detected null tilemap on a level '%s' layer instance '%s'. Please fix this in your LDTK project file." % [current_level, layer_name])
+ return tilemap
+
+ __place_tiles(tilemap, tiles, tile_source_id, grid_size)
+
+ return tilemap
+
+#endregion
+
+static func try_push_placeholder_ref(
+ placeholder: LDTKEntity,
+ entity: Node
+) -> void:
+ if not Util.options.resolve_entityrefs: return
+ if not placeholder.fields: return
+
+ placeholder.fields = str_to_var(var_to_str(placeholder.fields))
+ var field_defs = {}
+
+ for key in placeholder.definition.field_defs:
+ var def = placeholder.definition.field_defs[key]
+ field_defs[def.identifier] = def
+
+ for key in placeholder.fields:
+ var def = field_defs[key]
+ if not def.type.contains("EntityRef"): continue
+
+ var field = placeholder.fields[key]
+ if not field: continue
+
+ if field is Array:
+ for index in range(field.size()):
+ Util.add_unresolved_reference(field, index, entity)
+ else:
+ Util.add_unresolved_reference(placeholder.fields, key, entity)
+
+static func __place_tiles(
+ tilemap: TileMapLayer,
+ tiles: Array,
+ tile_source_id: int,
+ grid_size: Vector2,
+ layer_index: int = 0
+) -> void:
+
+ var tile_source: TileSetAtlasSource
+ if tilemap.tile_set.has_source(tile_source_id):
+ tile_source = tilemap.tile_set.get_source(tile_source_id)
+ else:
+ push_error("TileSetAtlasSource missing")
+ return
+
+ var tile_size := Vector2(tile_source.texture_region_size)
+
+ # Place tiles
+ for tile in tiles:
+ var cell_px := Vector2(tile.px[0], tile.px[1])
+ var tile_px := Vector2(tile.src[0], tile.src[1])
+ var cell_grid := TileUtil.px_to_grid(cell_px, grid_size, Vector2i.ZERO)
+ var tile_grid := TileUtil.px_to_grid(tile_px, tile_size, tile_source.margins, tile_source.separation)
+
+ # Tile does not exist
+ if not tile_source.has_tile(tile_grid):
+ continue
+
+ # Handle flipped tiles
+ var alternative_tile: int = 0
+ var tile_flip := TileUtil.get_tile_flip_mask(int(tile.f))
+
+ if (tile_flip != 0):
+ alternative_tile = tile_flip
+
+ # Handle alpha
+ if tile.a < 1.0:
+ var alternative_index := 1
+ var alternative_exists := false
+ var alternative_count := tile_source.get_alternative_tiles_count(tile_grid)
+
+ # Find alternate tile with same alpha
+ if alternative_count > alternative_index:
+ for i in range(alternative_index, alternative_count):
+ var data = tile_source.get_tile_data(tile_grid, i)
+ if is_equal_approx(data.modulate.a, tile.a):
+ # Reverse flip bools back into an int
+ var flip = TileUtil.get_tile_flip_mask(int(data.flip_h) + int(data.flip_v) * 2)
+ if tile_flip == flip:
+ alternative_index = i
+ alternative_exists = true
+ break
+
+ # Create new tile
+ if not alternative_exists:
+ alternative_index = tile_source.create_alternative_tile(tile_grid, alternative_count)
+ var new_data = tile_source.get_tile_data(tile_grid, alternative_index)
+ TileUtil.copy_and_modify_tile_data(
+ new_data,
+ tile_source.get_tile_data(tile_grid, 0),
+ tilemap.tile_set.get_physics_layers_count(),
+ tilemap.tile_set.get_navigation_layers_count(),
+ tilemap.tile_set.get_occlusion_layers_count(),
+ tile_flip
+ )
+ new_data.modulate.a = tile.a
+
+ alternative_tile = alternative_index
+
+ if not tilemap.get_cell_tile_data(cell_grid):
+ tilemap.set_cell(cell_grid, tile_source_id, tile_grid, alternative_tile)
+ else:
+ __place_overlapping_tile(tilemap, cell_grid, tile_source_id, tile_grid, alternative_tile)
+
+static func __place_overlapping_tile(
+ tilemap: TileMapLayer,
+ cell_grid: Vector2i,
+ tile_source_id: int,
+ tile_grid: Vector2i,
+ alternative_tile: int
+) -> void:
+ var tilemap_child: TileMapLayer
+ var empty := false
+
+ # Loop through existing children to find empty cell
+ for child in tilemap.get_children():
+ if not child.get_cell_tile_data(cell_grid):
+ tilemap_child = child
+ empty = true
+ break
+
+ # Create new child if no empty cell (or child) could be found
+ if not empty:
+ tilemap_child = LayerUtil.create_tilemap_child(tilemap)
+ tilemap.add_child(tilemap_child)
+
+ # Set tile
+ tilemap_child.set_cell(cell_grid, tile_source_id, tile_grid, alternative_tile)
diff --git a/godot/addons/ldtk-importer/src/layer.gd.uid b/godot/addons/ldtk-importer/src/layer.gd.uid
new file mode 100644
index 0000000..9942345
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/layer.gd.uid
@@ -0,0 +1 @@
+uid://46jgmlrl0r3s
diff --git a/godot/addons/ldtk-importer/src/level.gd b/godot/addons/ldtk-importer/src/level.gd
new file mode 100644
index 0000000..a4d8c0a
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/level.gd
@@ -0,0 +1,125 @@
+@tool
+
+const Util = preload("util/util.gd")
+const LevelUtil = preload("util/level-util.gd")
+const FieldUtil = preload("util/field-util.gd")
+const PostImport = preload("post-import.gd")
+const Layer = preload("layer.gd")
+
+static var base_directory: String
+
+static func build_levels(
+ world_data: Dictionary,
+ definitions: Dictionary,
+ base_dir: String,
+ external_levels: bool
+) -> Array[LDTKLevel]:
+
+ Util.timer_start(Util.DebugTime.GENERAL)
+ base_directory = base_dir
+ var levels: Array[LDTKLevel] = []
+
+ # Calculate level positions
+ var level_positions: Array
+ match world_data.worldLayout:
+ "LinearHorizontal":
+ var x = 0
+ for level in world_data.levels:
+ level_positions.append(Vector2i(x, 0))
+ x += level.pxWid
+ "LinearVertical":
+ var y := 0
+ for level in world_data.levels:
+ level_positions.append(Vector2i(0, y))
+ y += level.pxHei
+ "GridVania", "Free":
+ level_positions = world_data.levels.map(
+ func (current):
+ return Vector2i(current.worldX, current.worldY)
+ )
+ _:
+ printerr("World Layout not supported: ", world_data.worldLayout)
+
+ # Create levels
+ for level_index in range(world_data.levels.size()):
+ Util.timer_start(Util.DebugTime.GENERAL)
+ var level_data
+ var position: Vector2i = level_positions[level_index]
+ level_data = world_data.levels[level_index]
+
+ if external_levels:
+ level_data = LevelUtil.get_external_level(level_data, base_dir)
+
+ var level = create_level(level_data, position, definitions)
+ Util.timer_finish("Built Level", 2)
+
+ if (Util.options.entities_post_import):
+ level = PostImport.run_entity_post_import(level, Util.options.entities_post_import)
+
+ if (Util.options.level_post_import):
+ level = PostImport.run_level_post_import(level, Util.options.level_post_import)
+
+ levels.append(level)
+
+ Util.timer_finish("Built %s Levels" % levels.size(), 1)
+ return levels
+
+static func create_level(
+ level_data: Dictionary,
+ position: Vector2i,
+ definitions: Dictionary
+) -> LDTKLevel:
+ var level_name: String = level_data.identifier
+ var level := LDTKLevel.new()
+ level.name = level_name
+ level.iid = level_data.iid
+ level.world_position = position
+ level.size = Vector2i(level_data.pxWid, level_data.pxHei)
+ level.bg_color = level_data.__bgColor
+ level.z_index = level_data.worldDepth
+
+ if (Util.options.verbose_output): Util.print("block", level_name, 1)
+ Util.update_instance_reference(level_data.iid, level)
+
+ var neighbours = level_data.__neighbours
+
+ if not Util.options.pack_levels:
+ for neighbour in neighbours:
+ Util.add_unresolved_reference(neighbour, "levelIid", level)
+
+ level.neighbours = neighbours
+
+ # Create background image
+ if level_data.bgRelPath != null:
+ var path := "%s/%s" % [base_directory, level_data.bgRelPath]
+ var sprite := Sprite2D.new()
+ sprite.name = "BG Image"
+ sprite.centered = false
+ sprite.texture = load(path)
+
+ # Calculate BG Position
+ var bgData: Dictionary = level_data.__bgPos
+ var pos: Array = bgData.topLeftPx
+ var scale: Array = bgData.scale
+ var region: Array = bgData.cropRect
+ sprite.region_enabled = true
+ sprite.position = Vector2i(pos[0], pos[1])
+ sprite.scale = Vector2i(scale[0], scale[1])
+ sprite.region_rect = Rect2i(region[0], region[1], region[2], region[3])
+
+ level.add_child(sprite)
+
+ # Create fields
+ level.fields = FieldUtil.create_fields(level_data.fieldInstances, level)
+
+ var layer_instances = level_data.layerInstances
+ if not layer_instances is Array:
+ push_error("level '%s' has no layer instances." % [level_name])
+ return level
+
+ # Create layers
+ var layers = Layer.create_layers(level_data, layer_instances, definitions)
+ for layer in layers:
+ level.add_child(layer)
+
+ return level
diff --git a/godot/addons/ldtk-importer/src/level.gd.uid b/godot/addons/ldtk-importer/src/level.gd.uid
new file mode 100644
index 0000000..d4af28e
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/level.gd.uid
@@ -0,0 +1 @@
+uid://w5et03vhdql6
diff --git a/godot/addons/ldtk-importer/src/post-import.gd b/godot/addons/ldtk-importer/src/post-import.gd
new file mode 100644
index 0000000..a9c79d7
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/post-import.gd
@@ -0,0 +1,60 @@
+@tool
+
+const Util = preload("util/util.gd")
+
+static func run(element: Variant, script_path: String) -> Variant:
+ var element_type = typeof(element)
+
+ if not script_path.is_empty():
+ var script = load(script_path)
+ if not script or not script is GDScript:
+ printerr("Post-Import: '%s' is not a GDScript" % [script_path])
+ return ERR_INVALID_PARAMETER
+
+ script = script.new()
+ if not script.has_method("post_import"):
+ printerr("Post-Import: '%s' does not have a post_import() method" % [script_path])
+ return ERR_INVALID_PARAMETER
+
+ element = script.post_import(element)
+
+ if element == null or typeof(element) != element_type:
+ printerr("Post-Import: Invalid scene returned from script.")
+ return ERR_INVALID_DATA
+
+ return element
+
+static func run_tileset_post_import(tilesets: Dictionary, script_path: String) -> Dictionary:
+ Util.timer_start(Util.DebugTime.POST_IMPORT)
+ Util.print("tileset_post_import", str(tilesets), 1)
+ tilesets = run(tilesets, Util.options.tileset_post_import)
+ Util.timer_finish("Tileset Post-Import: Complete", 1)
+ return tilesets
+
+static func run_level_post_import(level: LDTKLevel, script_path: String) -> LDTKLevel:
+ Util.timer_start(Util.DebugTime.POST_IMPORT)
+ Util.print("level_post_import", level.name, 2)
+ level = run(level, Util.options.level_post_import)
+ Util.timer_finish("Level Post-Import: Complete", 2)
+ return level
+
+static func run_entity_post_import(level: LDTKLevel, script_path: String) -> LDTKLevel:
+ Util.timer_start(Util.DebugTime.POST_IMPORT)
+ var layers = level.get_children()
+ for layer in layers:
+ if layer is not LDTKEntityLayer:
+ continue
+
+ var entityLayerName = layer.get_parent().name + "." + layer.name
+ Util.print("entity_post_import", entityLayerName, 3)
+ layer = run(layer, script_path)
+
+ Util.timer_finish("Entity Post-Import: Complete", 3)
+ return level
+
+static func run_world_post_import(world: LDTKWorld, script_path: String) -> LDTKWorld:
+ Util.timer_start(Util.DebugTime.POST_IMPORT)
+ Util.print("world_post_import", world.name, 1)
+ world = run(world, Util.options.world_post_import)
+ Util.timer_finish("World Post-Import: Complete", 1)
+ return world
diff --git a/godot/addons/ldtk-importer/src/post-import.gd.uid b/godot/addons/ldtk-importer/src/post-import.gd.uid
new file mode 100644
index 0000000..1e81764
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/post-import.gd.uid
@@ -0,0 +1 @@
+uid://bv4fn2n2ncw12
diff --git a/godot/addons/ldtk-importer/src/tileset.gd b/godot/addons/ldtk-importer/src/tileset.gd
new file mode 100644
index 0000000..7f6f884
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/tileset.gd
@@ -0,0 +1,330 @@
+@tool
+
+const Util = preload("util/util.gd")
+const TileUtil = preload("util/tile-util.gd")
+const FieldUtil = preload("util/field-util.gd")
+const PostImport = preload("post-import.gd")
+
+enum AtlasTextureType {CompressedTexture2D, CanvasTexture}
+
+static func build_tilesets(
+ definitions: Dictionary,
+ base_dir: String,
+ tileset_overrides: Dictionary
+) -> Array:
+ Util.timer_start(Util.DebugTime.TILES)
+ var tilesets := {}
+ var tileset_sources := {}
+
+ # Reduce Layer Defs to find all unique layer grid sizes and create TileSets for each.
+ var layer_def_uids: Array = definitions.layers.keys()
+
+ tilesets = layer_def_uids.reduce(
+ func(accum: Dictionary, current: float):
+ var layer_def = definitions.layers[current]
+ var grid_size: int = layer_def.gridSize
+
+ if not accum.has(grid_size):
+ accum[grid_size] = get_tileset(grid_size, base_dir)
+
+ # Create TileSetSource for IntGrids
+ if (Util.options.integer_grid_tilesets):
+ if layer_def.type == "IntGrid" and layer_def.intGridValues.size() > 0:
+ var intgrid_uid = layer_def.uid
+ var intgrid_source = create_intgrid_source(layer_def)
+ tileset_sources[intgrid_uid] = intgrid_source
+ Util.add_tileset_reference(intgrid_uid, intgrid_source)
+ return accum
+ , tilesets)
+
+ # Create TileSetSources for each Tileset Def
+ var tileset_def_uids = definitions.tilesets.keys()
+ for uid in tileset_def_uids:
+ var tileset_def: Dictionary = definitions.tilesets[uid]
+ var source: TileSetSource = create_new_tileset_source(tileset_def, base_dir)
+ tileset_sources[uid] = source
+ Util.add_tileset_reference(tileset_def.uid, source)
+
+ # Add TileSetSources to TileSets
+ # NOTE: We also add Sources to mismatched TileSet sizes (if a layer uses that TilesetDef as an override)
+ for id in tilesets.keys():
+ var tileset: TileSet = tilesets[id]
+ var size: int = tileset.tile_size.x
+
+ for uid in tileset_sources.keys():
+ var source: TileSetAtlasSource = tileset_sources[uid]
+ if source == null: continue
+ var source_size: int = source.texture_region_size.x
+
+ # Check if override exists.
+ var has_override: bool = false
+ if (tileset_overrides.has(size)):
+ if (tileset_overrides[size].has(int(uid))):
+ has_override = true
+
+ # Include Source if size matches grid (or is an override found for this grid size)
+ if size == source_size or has_override:
+ if tileset.has_source(uid):
+ source = tileset.get_source(uid)
+ else:
+ source = source.duplicate()
+ tileset.add_source(source, uid)
+
+ if (Util.options.tileset_custom_data):
+ if definitions.tilesets.has(uid):
+ var tileset_def: Dictionary = definitions.tilesets[uid]
+ add_tileset_custom_data(tileset_def, tileset, source, tileset_def.__cWid)
+
+ # Post-Import
+ if (Util.options.tileset_post_import):
+ tilesets = PostImport.run_tileset_post_import(tilesets, Util.options.tileset_post_import)
+
+ # Store tilesets in Util
+ Util.tilesets = tilesets
+
+ Util.timer_finish("Tilesets Created", 1)
+
+ # Save tilesets
+ Util.timer_start(Util.DebugTime.SAVE)
+ var files = save_tilesets(tilesets, base_dir)
+ Util.timer_finish("Tilesets Saved", 1)
+
+ for key in tilesets.keys():
+ # reload tileset (improves performance)
+ var tileset = tilesets[key]
+ if tileset == null: continue
+ if not files.has(key): continue
+ tilesets[key] = ResourceLoader.load(files[key])
+
+ return files.values()
+
+static func get_tileset(tile_size: int,base_dir: String) -> TileSet:
+ var tileset_name := "tileset_%spx" % [str(tile_size)]
+ var path := base_dir + "tilesets/" + tileset_name + ".res"
+
+ if not (Util.options.force_tileset_reimport):
+ if ResourceLoader.exists(path):
+ var tileset = ResourceLoader.load(path)
+ if tileset is TileSet:
+ return tileset
+
+ # Create new TileSet
+ var tileset := TileSet.new()
+ tileset.resource_name = tileset_name
+ tileset.tile_size = Vector2i(tile_size, tile_size)
+
+ if (Util.options.verbose_output):
+ Util.print("item_info", "Created new TileSet: \"%s\"" % [tileset_name], 1)
+
+ return tileset
+
+# Create an AtlasSource using tileset definition
+static func create_new_tileset_source(definition: Dictionary, base_dir: String) -> TileSetSource:
+ # No source texture defined
+ if definition.relPath == null:
+ Util.print("item_fail", "No texture defined for tileset '%s'" % [definition.identifier])
+ push_error("Tileset Definition '%s' has no source texture. Please fix this in your LDtk project file." % [definition.identifier])
+ return null
+
+ # Check if relPath is an absolute directory
+ var filepath: String
+ if definition.relPath.contains(":"):
+ push_warning("Absolute path detected for texture resource '%s'. This is not recommended. Please include this file in the Godot project." % [definition.identifier])
+ filepath = definition.relPath
+ else:
+ filepath = base_dir + definition.relPath
+
+ var texture := load(filepath)
+
+ # Cannot load texture
+ if texture == null:
+ push_error("Cannot access source texture: %s. Please include this file in the Godot project." % [filepath])
+ return null
+
+ var image: Image = texture.get_image()
+
+ # Convert texture from CompressedTexture2D to CanvasTexture
+ if (Util.options.atlas_texture_type == AtlasTextureType.CanvasTexture):
+ var canvas_texture = CanvasTexture.new()
+ canvas_texture.diffuse_texture = texture
+ texture = canvas_texture
+
+ var tile_size: int = definition.gridSize
+ var margin: int = definition.padding
+ var separation: int = definition.spacing
+ var grid_w: int = definition.__cWid
+ var grid_h: int = definition.__cHei
+
+ var source := TileSetAtlasSource.new()
+
+ # Apply TileSet properties
+ if source.texture == null or source.texture.get_class() != texture.get_class():
+ source.texture = texture
+
+ source.resource_name = definition.identifier
+ source.margins = Vector2i(margin, margin)
+ source.separation = Vector2i(separation, separation)
+ source.texture_region_size = Vector2(tile_size, tile_size)
+ source.use_texture_padding = false
+
+ # Create/remove tiles in non-empty/empty cells.
+ for y in range(0, grid_h):
+ for x in range(0, grid_w):
+ var coords := Vector2i(x,y)
+ var tile_region := TileUtil.get_tile_region(coords, tile_size, margin, separation, grid_w)
+ var tile_image := image.get_region(tile_region)
+
+ if not tile_image.is_invisible():
+ if source.get_tile_at_coords(coords) == Vector2i(-1,-1):
+ source.create_tile(coords)
+ elif not source.get_tile_at_coords(coords) == Vector2i(-1,-1):
+ # TODO: Make this an import flag
+ source.remove_tile(coords)
+
+ # Add definition UID to references
+ Util.add_tileset_reference(definition.uid, source)
+
+ return source
+
+static func add_tileset_custom_data(
+ definition: Dictionary,
+ tileset: TileSet,
+ source: TileSetAtlasSource,
+ grid_w: int
+) -> void:
+
+ if not definition.has("enumTags"):
+ return
+
+ var customData: Array = definition.customData
+ var custom_name: String = "LDTK Custom"
+ clear_custom_data(tileset, custom_name)
+
+ if not customData.is_empty():
+ ensure_custom_layer(tileset, custom_name)
+ for entry in customData:
+ var coords := TileUtil.tileid_to_grid(entry.tileId, grid_w)
+ var tile_data: TileData = source.get_tile_data(coords, 0)
+ if not tile_data == null:
+ tile_data.set_custom_data(custom_name, entry.data)
+
+ var custom_enum_name: String = "LDTK Custom Enum"
+ clear_custom_data(tileset, custom_enum_name)
+
+ var enumTags: Array = definition.enumTags
+ if not enumTags.is_empty():
+ ensure_custom_layer(tileset, custom_enum_name, TYPE_ARRAY)
+
+ for enumTag in enumTags:
+ for tileId in enumTag.tileIds:
+ var coords := TileUtil.tileid_to_grid(tileId, grid_w)
+ var tile_data: TileData = source.get_tile_data(coords, 0)
+ if not tile_data == null:
+ # Add to already existing tags
+ var tile_tags: Array = tile_data.get_custom_data(custom_enum_name)
+ tile_tags.append(enumTag.enumValueId)
+ tile_data.set_custom_data(custom_enum_name, tile_tags)
+
+# Ensure custom data layer exists by name
+static func ensure_custom_layer(
+ tileset: TileSet,
+ layer_name: String,
+ layer_type: int = TYPE_STRING
+) -> void:
+ if tileset.get_custom_data_layer_by_name(layer_name) != -1:
+ return
+ var index_to_add = tileset.get_custom_data_layers_count()
+ tileset.add_custom_data_layer(index_to_add)
+ tileset.set_custom_data_layer_name(index_to_add, layer_name)
+ tileset.set_custom_data_layer_type(index_to_add, layer_type)
+
+# Clear custom data by layer name
+static func clear_custom_data(tileset: TileSet, layer_name: String) -> void:
+ var layer = tileset.get_custom_data_layer_by_name(layer_name)
+ if layer == -1:
+ return
+ tileset.remove_custom_data_layer(layer)
+
+# Create an AtlasSource from IntGrid data
+static func create_intgrid_source(definition: Dictionary) -> TileSetAtlasSource:
+ var values: Array = definition.intGridValues
+ var grid_size: int = definition.gridSize
+
+ # Create texture from IntGrid values
+ var width := grid_size * values.size()
+ var height := grid_size
+ var image := Image.create(width, height, false, Image.FORMAT_RGB8)
+
+ for index in range(0, values.size()):
+ var value: Dictionary = values[index]
+ var rect := Rect2i(index * grid_size, 0, grid_size, grid_size)
+ var color := Color.from_string(value.color, Color.MAGENTA)
+ image.fill_rect(rect, color)
+
+ var texture = ImageTexture.create_from_image(image)
+
+ var source := TileSetAtlasSource.new()
+ source.resource_name = definition.identifier + "_Tiles"
+ source.texture = texture
+ source.texture_region_size = Vector2i(grid_size, grid_size)
+
+ # Create tiles
+ for index in range(0, values.size()):
+ var coords := Vector2i(index, 0)
+ if not source.has_tile(coords):
+ source.create_tile(coords)
+
+ return source
+
+# Save TileSets as Resources
+static func save_tilesets(tilesets: Dictionary, base_dir: String) -> Dictionary:
+ var save_path = base_dir + "tilesets/"
+ var gen_files := {}
+ var directory = DirAccess.open(base_dir)
+ if not directory.dir_exists(save_path):
+ directory.make_dir_recursive(save_path)
+
+ var tileset_names = tilesets.values().map(func(elem): return elem.resource_name)
+ Util.print("item_save", "Saving Tilesets: [color=#fe8019]%s[/color]" % [tileset_names], 1)
+
+ for key in tilesets.keys():
+ var tileset: TileSet = tilesets.get(key)
+ if tileset.get_source_count() == 0:
+ continue
+
+ var file_name = tileset.resource_name
+ var file_path = "%s%s.%s" % [save_path, file_name, "res"]
+ var err = ResourceSaver.save(tileset, file_path)
+ if err == OK:
+ gen_files[key] = file_path
+
+ return gen_files
+
+static func get_entity_def_tiles(definitions: Dictionary, tilesets: Dictionary) -> Dictionary:
+ # TODO: Loop through EntityDefs, and turn 'Tile' into an Image.
+ for def in definitions.entities:
+ var entity: Dictionary = definitions.entities[def]
+ if (entity.tile == null):
+ continue
+ # Find associated TileSet
+ var texture = FieldUtil.__parse_tile(entity.tile)
+ entity.tile = texture
+
+ return definitions
+
+# Collect all layer tileset overrides. Later we'll ensure these sources are included in TileSet resources.
+static func get_tileset_overrides(world_data: Dictionary) -> Dictionary:
+ var overrides := {}
+ for level in world_data.levels:
+ for layer in level.layerInstances:
+ if layer.overrideTilesetUid == null:
+ continue
+ var gridSize: int = layer.__gridSize
+ var overrideUid: int = layer.overrideTilesetUid
+ if overrideUid != null:
+ if not overrides.has(gridSize):
+ overrides[gridSize] = []
+ var gridsize_overrides: Array = overrides[gridSize]
+ if not gridsize_overrides.has(overrideUid):
+ gridsize_overrides.append(overrideUid)
+ return overrides
diff --git a/godot/addons/ldtk-importer/src/tileset.gd.uid b/godot/addons/ldtk-importer/src/tileset.gd.uid
new file mode 100644
index 0000000..6e088eb
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/tileset.gd.uid
@@ -0,0 +1 @@
+uid://etx2miclpo5a
diff --git a/godot/addons/ldtk-importer/src/util/definition_util.gd b/godot/addons/ldtk-importer/src/util/definition_util.gd
new file mode 100644
index 0000000..2db8b41
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/definition_util.gd
@@ -0,0 +1,108 @@
+@tool
+
+const Util = preload("util.gd")
+
+# When building definitions we are only collecting data we need and do some pre-parsing to make the next steps easier.
+
+static func build_definitions(world_data: Dictionary) -> Dictionary:
+ var definitions := {
+ "enums": resolve_enum_definitions(world_data.defs.enums),
+ "entities": resolve_entity_definitions(world_data.defs.entities),
+ "layers": resolve_layer_definitions(world_data.defs.layers),
+ "tilesets": resolve_tileset_definitions(world_data.defs.tilesets, world_data.defs.layers),
+ "level_fields": resolve_level_field_definitions(world_data.defs.levelFields),
+ }
+ return definitions
+
+static func resolve_layer_definitions(layer_defs: Array) -> Dictionary:
+ var resolved_layer_defs := {}
+
+ for layer_def in layer_defs:
+ resolved_layer_defs[layer_def.uid] = {
+ "uid": layer_def.uid,
+ "type": layer_def.type,
+ "identifier": layer_def.identifier,
+ "gridSize": layer_def.gridSize,
+ "offset": Vector2i(layer_def.pxOffsetX, layer_def.pxOffsetY),
+ "parallax": Vector2(layer_def.parallaxFactorX, layer_def.parallaxFactorY),
+ "parallaxScaling": layer_def.parallaxScaling,
+ "intGridValues": layer_def.intGridValues
+ }
+
+ return resolved_layer_defs
+
+static func resolve_entity_definitions(entity_defs: Array) -> Dictionary:
+ var resolved_entity_defs := {}
+
+ for entity_def in entity_defs:
+ resolved_entity_defs[entity_def.uid] = {
+ "identifier": entity_def.identifier,
+ "color": Color.from_string(entity_def.color, Color.MAGENTA),
+ "renderMode": entity_def.renderMode,
+ "hollow": entity_def.hollow,
+ "tags": entity_def.tags,
+ "field_defs": resolve_entity_field_defs(entity_def.fieldDefs),
+ "tile": entity_def.uiTileRect
+ }
+
+ return resolved_entity_defs
+
+static func resolve_entity_field_defs(field_defs: Array) -> Dictionary:
+ var resolved_entity_field_defs := {}
+
+ for field_def in field_defs:
+ resolved_entity_field_defs[int(field_def.uid)] = {
+ "identifier": field_def.identifier,
+ "type": field_def.__type,
+ }
+
+ return resolved_entity_field_defs
+
+static func resolve_tileset_definitions(tileset_defs: Array, layer_defs: Array) -> Dictionary:
+ var resolved_tileset_defs := {}
+
+ for tileset_def in tileset_defs:
+ resolved_tileset_defs[tileset_def.uid] = {
+ "uid": tileset_def.uid,
+ "identifier": tileset_def.identifier,
+ "relPath": tileset_def.relPath,
+ "gridSize": tileset_def.tileGridSize,
+ "pxWid": tileset_def.pxWid,
+ "pxHei": tileset_def.pxHei,
+ "spacing": tileset_def.spacing,
+ "padding": tileset_def.padding,
+ "tags": tileset_def.tags,
+ "enumTagUid": tileset_def.tagsSourceEnumUid,
+ "enumTags": tileset_def.enumTags,
+ "customData": tileset_def.customData,
+ "__cWid": tileset_def.__cWid,
+ "__cHei": tileset_def.__cHei,
+ }
+ return resolved_tileset_defs
+
+static func resolve_enum_definitions(enum_defs: Array) -> Dictionary:
+ var resolved_enum_defs := {}
+
+ for enum_def in enum_defs:
+ var uid = enum_def.uid
+ var values := []
+ for value in enum_def.values:
+ values.append({
+ "value": value.id,
+ "color": Color.from_string(str(value.color), Color.MAGENTA),
+ "tileRect": value.tileRect
+ })
+ resolved_enum_defs[uid] = values
+
+ return resolved_enum_defs
+
+static func resolve_level_field_definitions(level_field_defs: Array) -> Dictionary:
+ var resolved_level_field_defs := {}
+
+ for level_field_def in level_field_defs:
+ resolved_level_field_defs[level_field_def.uid] = {
+ "identifier": level_field_def.identifier,
+ "type": level_field_def.__type
+ }
+
+ return resolved_level_field_defs
diff --git a/godot/addons/ldtk-importer/src/util/definition_util.gd.uid b/godot/addons/ldtk-importer/src/util/definition_util.gd.uid
new file mode 100644
index 0000000..cb0b456
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/definition_util.gd.uid
@@ -0,0 +1 @@
+uid://10dqdfkkqwa2
diff --git a/godot/addons/ldtk-importer/src/util/field-util.gd b/godot/addons/ldtk-importer/src/util/field-util.gd
new file mode 100644
index 0000000..1fcb7c8
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/field-util.gd
@@ -0,0 +1,126 @@
+@tool
+
+const Util = preload("util.gd")
+const TileUtil = preload("tile-util.gd")
+
+static var hitUnresolved := false
+static var current_field_name: String = "N/A"
+
+static func create_fields(fields: Array, entity: Variant = null) -> Dictionary:
+ var dict := {}
+ for field in fields:
+ var key: String = field.__identifier
+ current_field_name = key
+ dict[key] = parse_field(field)
+
+ if not Util.options.resolve_entityrefs:
+ hitUnresolved = false
+ continue
+
+ if hitUnresolved and entity != null:
+ if dict[key] is Array:
+ for index in range(dict[key].size()):
+ Util.add_unresolved_reference(dict[key], index, entity)
+ else:
+ Util.add_unresolved_reference(dict, key, entity)
+ hitUnresolved = false
+
+ return dict
+
+static func parse_field(field: Dictionary) -> Variant:
+ var value: Variant = field.__value
+ if value == null:
+ return null
+
+ var type := field.__type as String
+
+ # Handle enum string
+ var localEnum: String
+ if type.contains("LocalEnum"):
+ var regex = RegEx.new()
+ regex.compile("(?<=\\.)\\w+")
+ localEnum = regex.search(type).get_string()
+
+ if type.begins_with("Array"):
+ type = "Array"
+ else:
+ type = "LocalEnum"
+
+ # Match field type
+ match type:
+ "Int":
+ return int(value) as int
+ "Color":
+ return Color.from_string(value, Color.MAGENTA) as Color
+ "Point":
+ return __parse_point(value.cx, value.cy) as Vector2i
+ "Tile":
+ return __parse_tile(value) as AtlasTexture
+ "EntityRef":
+ hitUnresolved = true
+ return value.entityIid as String
+ "LocalEnum":
+ return __parse_enum(localEnum, value) as String
+ "Array":
+ return value
+ "Array":
+ return value.map(
+ func (color):
+ return Color.from_string(color, Color.MAGENTA)
+ )
+ "Array":
+ return value.map(
+ func (point):
+ return Vector2i(point.cx, point.cy)
+ )
+ "Array":
+ return value.map(
+ func(tile) -> AtlasTexture:
+ return __parse_tile(tile)
+ )
+ "Array":
+ hitUnresolved = true
+ return value.map(
+ func (ref) -> String:
+ return ref.entityIid
+ )
+ "Array":
+ var enums: Array[String] = []
+ for index in range(value.size()):
+ var parsed_enum = __parse_enum(localEnum, value[index])
+ enums.append(parsed_enum)
+ return enums
+ _:
+ return value
+
+static func __parse_point(x: int, y: int) -> Vector2i:
+ # NOTE: would convert gridcoords to pixelcoords here, but needs more data
+ # LDTKEntity currently converts it using LayerDefinition.
+ return Vector2i(x,y)
+
+static func __parse_enum(enum_str: String, value: String) -> String:
+ var result: String = "%s.%s" % [enum_str, value]
+ return result
+
+static func __parse_tile(value: Dictionary) -> AtlasTexture:
+ var texture := AtlasTexture.new()
+ var atlas: TileSetAtlasSource
+
+ if Util.tileset_refs.has(int(value.tilesetUid)):
+ atlas = Util.tileset_refs[int(value.tilesetUid)]
+
+ if atlas == null :
+ print("Tileset Refs: ", Util.tileset_refs)
+ print("FieldDef '%s' could not find Tileset with UID '%s'. Using empty texture instead. Please fix this in your LDtk file." % [current_field_name, value.tilesetUid])
+ return texture
+
+ texture.atlas = atlas.texture
+
+ var coords = TileUtil.px_to_grid(
+ Vector2(value.x, value.y),
+ atlas.texture_region_size,
+ atlas.margins,
+ atlas.separation
+ )
+ texture.region = atlas.get_tile_texture_region(coords) as Rect2i
+ return texture
diff --git a/godot/addons/ldtk-importer/src/util/field-util.gd.uid b/godot/addons/ldtk-importer/src/util/field-util.gd.uid
new file mode 100644
index 0000000..d7ec746
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/field-util.gd.uid
@@ -0,0 +1 @@
+uid://dp7qwjbyo1kx0
diff --git a/godot/addons/ldtk-importer/src/util/layer-util.gd b/godot/addons/ldtk-importer/src/util/layer-util.gd
new file mode 100644
index 0000000..b82d35f
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/layer-util.gd
@@ -0,0 +1,68 @@
+@tool
+
+const Util = preload("util.gd")
+const EntityPlaceHolder = preload("../components/ldtk-entity.tscn")
+const FieldUtil = preload("field-util.gd")
+
+# Counter reset per level, used when creating EntityPlacholders
+static var placeholder_counts := {}
+
+static func parse_entity_instances(
+ entities: Array,
+ entity_defs: Dictionary,
+ pathResolver: Node2D
+) -> Array:
+ return entities.map(
+ func(entity):
+ var definition = entity_defs[entity.defUid]
+ return {
+ "iid": entity.iid,
+ "identifier": entity.__identifier,
+ "smart_color": Color.from_string(entity.__smartColor, Color.WHITE),
+ "size": Vector2i(entity.width, entity.height),
+ "position": Vector2i(entity.px[0], entity.px[1]),
+ "pivot": Vector2(entity.__pivot[0], entity.__pivot[1]),
+ "fields": FieldUtil.create_fields(entity.fieldInstances, pathResolver),
+ "definition": definition,
+ }
+ )
+
+static func create_entity_placeholder(layer: Node2D, data: Dictionary) -> LDTKEntity:
+ var placeholder: LDTKEntity = EntityPlaceHolder.instantiate()
+ var count: int = __placeholder_count(data.identifier)
+ placeholder.name = data.identifier
+
+ if count > 1:
+ placeholder.name += str(count)
+
+ # Set properties
+ for prop in data.keys():
+ placeholder[prop] = data[prop]
+
+ layer.add_child(placeholder)
+ return placeholder
+
+static func __placeholder_count(name: String) -> int:
+ if not name in placeholder_counts:
+ placeholder_counts[name] = 1
+ else:
+ placeholder_counts[name] += 1
+ return placeholder_counts[name]
+
+static func create_layer_tilemap(layer_data: Dictionary) -> TileMapLayer:
+ var grid_size = int(layer_data.__gridSize)
+
+ var tilemap := TileMapLayer.new()
+ tilemap.name = layer_data.__identifier
+ tilemap.tile_set = Util.tilesets.get(grid_size, null)
+ var offset = Vector2(layer_data.__pxTotalOffsetX, layer_data.__pxTotalOffsetY)
+ tilemap.position = offset
+
+ return tilemap
+
+static func create_tilemap_child(tilemap: TileMapLayer) -> TileMapLayer:
+ var child := TileMapLayer.new()
+ var count := tilemap.get_child_count() + 1
+ child.name = tilemap.name + str(count)
+ child.tile_set = tilemap.tile_set
+ return child
diff --git a/godot/addons/ldtk-importer/src/util/layer-util.gd.uid b/godot/addons/ldtk-importer/src/util/layer-util.gd.uid
new file mode 100644
index 0000000..93ebfa9
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/layer-util.gd.uid
@@ -0,0 +1 @@
+uid://cm8kt5fyu4max
diff --git a/godot/addons/ldtk-importer/src/util/level-util.gd b/godot/addons/ldtk-importer/src/util/level-util.gd
new file mode 100644
index 0000000..348bc8a
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/level-util.gd
@@ -0,0 +1,54 @@
+@tool
+
+const Util = preload("util.gd")
+
+static func get_world_position(
+ world_data: Dictionary,
+ level_data: Dictionary
+) -> Vector2i:
+
+ var layout = world_data.worldLayout
+
+ if layout == "GridVania" or layout == "Free":
+ return Vector2i(level_data.worldX, level_data.worldY)
+ elif layout == "LinearHorizontal" or layout == "LinearVertical":
+ # List level uids in order.
+ var level_uids: Array = world_data.levels.map(
+ func(item):
+ return item.uid
+ )
+ # Find level index
+ var index = level_uids.find(level_data.uid)
+ if index == 0:
+ return Vector2i(0,0)
+
+ if layout == "LinearHorizontal":
+ var x: int = world_data.levels.slice(0, index).reduce(
+ func (accum, current):
+ return accum + current.pxWid
+ , 0)
+ return Vector2i(x, 0)
+ else:
+ var y: int = world_data.levels.slice(0, index).reduce(
+ func (accum, current):
+ return accum + current.pHei
+ , 0)
+ return Vector2i(0, y)
+ else:
+ push_warning("World layout not supported", world_data.worldLayout)
+ return Vector2i.ZERO
+
+
+static func get_external_level(
+ level_data: Dictionary,
+ base_dir: String
+) -> Dictionary:
+
+ var level_file: String = base_dir + level_data.externalRelPath
+ var new_level_data: Dictionary = Util.parse_file(level_file)
+ if not new_level_data == null:
+ return new_level_data
+ else:
+ push_warning("Could not parse external level: ", level_file)
+
+ return level_data
diff --git a/godot/addons/ldtk-importer/src/util/level-util.gd.uid b/godot/addons/ldtk-importer/src/util/level-util.gd.uid
new file mode 100644
index 0000000..7c25a3c
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/level-util.gd.uid
@@ -0,0 +1 @@
+uid://oc513xtwqvf1
diff --git a/godot/addons/ldtk-importer/src/util/tile-util.gd b/godot/addons/ldtk-importer/src/util/tile-util.gd
new file mode 100644
index 0000000..7b863db
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/tile-util.gd
@@ -0,0 +1,126 @@
+@tool
+
+# Flip a vector based on a bitset
+static func _flip_vector_array_with_bitset(
+ vecs: PackedVector2Array,
+ bitset: int
+) -> PackedVector2Array:
+ var new_vecs = PackedVector2Array(vecs)
+ for point_idx in range(vecs.size()):
+ var new_vec = Vector2(vecs[point_idx])
+ if bitset & 1:
+ new_vec.x = -new_vec.x
+ if bitset & 2:
+ new_vec.y = -new_vec.y
+ new_vecs[point_idx] = new_vec
+ return new_vecs
+
+# Copy over and rotate extra tiledata
+static func copy_and_modify_tile_data(
+ tile_data: TileData,
+ orig_tile_data: TileData,
+ physics_layers_cnt: int,
+ navigation_layers_cnt: int,
+ occluder_layers_cnt: int,
+ bitset: int,
+) -> void:
+ # Copy over physics
+ for pli in range(physics_layers_cnt):
+ var polygon_cnt = orig_tile_data.get_collision_polygons_count(pli)
+ if polygon_cnt == 0:
+ # We have no polygon for this layer
+ continue
+ for pi in range(polygon_cnt):
+ tile_data.add_collision_polygon(pli)
+ var points: PackedVector2Array = _flip_vector_array_with_bitset(orig_tile_data.get_collision_polygon_points(pli, pi), bitset)
+ tile_data.set_collision_polygon_points(pli, pi, points)
+ tile_data.set_constant_angular_velocity(pli, orig_tile_data.get_constant_angular_velocity(pli))
+ var linvel = Vector2(orig_tile_data.get_constant_linear_velocity(pli))
+ if bitset & TileSetAtlasSource.TRANSFORM_FLIP_H:
+ linvel.x = -linvel.x
+ if bitset & TileSetAtlasSource.TRANSFORM_FLIP_V:
+ linvel.y = -linvel.y
+ tile_data.set_constant_linear_velocity(pli, linvel)
+
+ # Copy over navigation
+ for navi in range(navigation_layers_cnt):
+ var nav_polygon: NavigationPolygon = orig_tile_data.get_navigation_polygon(navi)
+ if nav_polygon == null:
+ # We have no polygon for this layer
+ continue
+ var new_polygon = NavigationPolygon.new()
+ for outline_idx in range(nav_polygon.get_outline_count()):
+ var vertices = _flip_vector_array_with_bitset(nav_polygon.get_outline(outline_idx), bitset)
+ new_polygon.add_outline(vertices)
+ new_polygon.make_polygons_from_outlines()
+ tile_data.set_navigation_polygon(navi, new_polygon)
+
+ # Copy over occluder
+ for occi in range(occluder_layers_cnt):
+ var occluder: OccluderPolygon2D = orig_tile_data.get_occluder(occi)
+ if occluder == null:
+ # We have no polygon for this layer
+ continue
+ var new_occluder: OccluderPolygon2D = OccluderPolygon2D.new()
+ new_occluder.cull_mode = occluder.cull_mode
+ new_occluder.closed = occluder.closed
+ new_occluder.polygon = _flip_vector_array_with_bitset(occluder.polygon, bitset)
+ tile_data.set_occluder(occi, new_occluder)
+
+ # Flip depending on bitset
+ if bitset & TileSetAtlasSource.TRANSFORM_FLIP_H:
+ tile_data.set_flip_h(true)
+ if bitset & TileSetAtlasSource.TRANSFORM_FLIP_V:
+ tile_data.set_flip_v(true)
+
+# Get Rect of Tile for an AtlasSource using LDTK tileset data
+static func get_tile_region(
+ coords: Vector2i,
+ grid_size: int,
+ padding: int,
+ spacing: int,
+ grid_w: int
+) -> Rect2i:
+ var pixel_coords = grid_to_px(coords, grid_size, padding, spacing)
+ return Rect2i(pixel_coords, Vector2i(grid_size, grid_size))
+
+# Convert grid coords to pixel coords
+static func grid_to_px(
+ grid_coords: Vector2i,
+ grid_size: int,
+ padding: int,
+ spacing: int
+) -> Vector2i:
+ var x: int = padding + grid_coords.x * (grid_size + spacing)
+ var y: int = padding + grid_coords.y * (grid_size + spacing)
+ return Vector2i(x, y)
+
+# Converts px coords to grid coords
+static func px_to_grid(
+ px_coords: Vector2,
+ grid_size: Vector2,
+ padding: Vector2 = Vector2.ZERO,
+ spacing: Vector2 = Vector2.ZERO
+) -> Vector2i:
+ var x: int = round((px_coords.x - padding.x) / (grid_size.x + spacing.x))
+ var y: int = round((px_coords.y - padding.y) / (grid_size.y + spacing.y))
+ return Vector2i(x, y)
+
+# Convert TileId to grid coords
+static func tileid_to_grid(tile_id: int, grid_w: int) -> Vector2i:
+ var y := int(tile_id / grid_w)
+ var x := tile_id % grid_w
+ return Vector2i(x, y)
+
+static func index_to_grid(index: int, grid_w: int) -> Vector2i:
+ var x: int = floor(index % grid_w)
+ var y: int = floor(index / grid_w)
+ return Vector2i(x, y)
+
+# Converts '1' to (TRANSFORM_FLIP_H), and so on.
+static func get_tile_flip_mask(index: int) -> int:
+ # 0 -> 0
+ # 1 -> TileSetAtlasSource.TRANSFORM_FLIP_H (4096)
+ # 2 -> TileSetAtlasSource.TRANSFORM_FLIP_V (8192)
+ # 3 -> TileSetAtlasSource.TRANSFORM_FLIP_H | TileSetAtlasSource.TRANSFORM_FLIP_H (12_228)
+ return index << 12
diff --git a/godot/addons/ldtk-importer/src/util/tile-util.gd.uid b/godot/addons/ldtk-importer/src/util/tile-util.gd.uid
new file mode 100644
index 0000000..f2575d9
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/tile-util.gd.uid
@@ -0,0 +1 @@
+uid://bp3rb0p6dsayi
diff --git a/godot/addons/ldtk-importer/src/util/time-util.gd b/godot/addons/ldtk-importer/src/util/time-util.gd
new file mode 100644
index 0000000..2de990b
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/time-util.gd
@@ -0,0 +1,48 @@
+@tool
+
+## Helper Script to track performance of the importer.
+
+enum { SAVE, LOAD, GENERAL, TILES, POST_IMPORT, TOTAL }
+
+static var category_time := {
+ SAVE: 0,
+ LOAD: 0,
+ GENERAL: 0,
+ TILES: 0,
+ POST_IMPORT: 0,
+ TOTAL : 0,
+}
+
+static var category_name := {
+ SAVE: "save",
+ LOAD: "load",
+ GENERAL : "general",
+ TILES: "tiles",
+ POST_IMPORT: "post-import",
+ TOTAL: "total"
+}
+
+static func log_time(category: int, time: int = 0) -> void:
+ if category_time.has(category):
+ category_time[category] += time
+ else:
+ push_warning("No DebugTime Category '%s'" % [category_name[category]])
+
+static func clear_time() -> void:
+ for category in category_time:
+ category_time[category] = 0
+
+static func get_total_time() -> int:
+ var sum: int = 0
+ for category in category_time:
+ if category != TOTAL:
+ sum += category_time[category]
+ return sum
+
+static func get_result() -> String:
+ var result: String = "Performance Results:"
+ for category in category_time:
+ if category != TOTAL:
+ result += "\n [color=#8ec07c]%s [color=slategray](%sms)[/color]" % [category_name[category], category_time[category]]
+ result.indent("\t")
+ return result
diff --git a/godot/addons/ldtk-importer/src/util/time-util.gd.uid b/godot/addons/ldtk-importer/src/util/time-util.gd.uid
new file mode 100644
index 0000000..a0a8c67
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/time-util.gd.uid
@@ -0,0 +1 @@
+uid://bynwlcmpnsjit
diff --git a/godot/addons/ldtk-importer/src/util/util.gd b/godot/addons/ldtk-importer/src/util/util.gd
new file mode 100644
index 0000000..b66f1b6
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/util.gd
@@ -0,0 +1,226 @@
+@tool
+
+const DebugTime = preload("time-util.gd")
+
+enum LDTK_VERSION {
+ FUTURE,
+ v1_5,
+ v1_4,
+ v1_3,
+ v1_2,
+ v1_0,
+ UNSUPPORTED
+}
+static var file_version = LDTK_VERSION.UNSUPPORTED
+
+# Stores import flags (used throughout the importer)
+static var options := {}
+
+static func parse_file(source_file: String) -> Dictionary:
+ var json := FileAccess.open(source_file, FileAccess.READ)
+ if json == null:
+ push_error("\nFailed to open file: ", source_file)
+ return {}
+ var data := JSON.parse_string(json.get_as_text())
+ return data
+
+static func check_version(version: String, latest_version: String) -> bool:
+ if version.begins_with("0."):
+ push_error("LDTK version out of date. Please update LDtk to ", latest_version)
+ file_version = LDTK_VERSION.UNSUPPORTED
+ return false
+
+ var major_minor = version.substr(0, 3)
+ match major_minor:
+ "1.0", "1.1":
+ file_version = LDTK_VERSION.v1_0
+ "1.2":
+ file_version = LDTK_VERSION.v1_2
+ "1.3":
+ file_version = LDTK_VERSION.v1_3
+ "1.4":
+ file_version = LDTK_VERSION.v1_4
+ "1.5":
+ file_version = LDTK_VERSION.v1_5
+ _:
+ push_warning("LDtk file version is newer than what is supported. Errors may occur.")
+ file_version = LDTK_VERSION.FUTURE
+ return true
+
+static func recursive_set_owner(node: Node, owner: Node) -> void:
+ node.set_owner(owner)
+ for child in node.get_children():
+ # Child is NOT an instantiated scene - this would otherwise cause errors
+ if child.scene_file_path == "":
+ recursive_set_owner(child, owner)
+ else:
+ child.set_owner(owner)
+
+#region Performance Measurement
+
+static var last_time: int = 0
+static var time_history: Array[Dictionary] = []
+
+static func timer_start(category: int = 0) -> int:
+ var t: int = Time.get_ticks_msec()
+ var d: int = t - last_time
+ last_time = t
+
+ if time_history.size() > 0:
+ # Entering subcategory - log prev category up to here
+ var last: Dictionary = time_history[-1]
+ DebugTime.log_time(last.category, d)
+
+ time_history.append({"category": category, "time": t, "init": t})
+ return t
+
+static func timer_finish(message: String, indent: int = 0, doPrint: bool = true) -> int:
+ if time_history.size() == 0:
+ push_error("Unbalanced DebugTime stack")
+ var last: Dictionary = time_history.pop_back()
+ var t: int = Time.get_ticks_msec()
+ var d: int = t - last.time
+ last_time = t
+ DebugTime.log_time(last.category, d)
+
+ if time_history.size() > 0:
+ time_history[-1].time = t
+
+ if (doPrint and options.verbose_output):
+ # Print 'gross' duration for this block
+ var d2: int = t - last.init
+ print_time("item_info_time", message, d2, indent)
+ return d
+
+static func timer_reset() -> void:
+ last_time = 0
+ time_history.clear()
+ DebugTime.clear_time()
+
+#endregion
+
+#region Debug Output
+
+const PRINT_SNIPPET := {
+ "import_start": "[bgcolor=#ffcc00][color=black][LDTK][/color][/bgcolor][color=#ffcc00] Start Import: [color=#fe8019][i]'%s'[/i][/color]",
+ "import_finish": "[bgcolor=#ffcc00][color=black][LDTK][/color][/bgcolor][color=#ffcc00] Finished Import. [color=slategray](Total Time: %sms)[/color]",
+ "item_ok" : "[color=#b8bb26]• %s ✔[/color]",
+ "item_fail": "[color=#fb4934]• %s ✘[/color]",
+ "item_info": "[color=#8ec07c]• %s [/color]",
+ "item_save": "[color=#ffcc00]• %s [/color]",
+ "item_post_import": "[color=tomato]‣ %s[/color]",
+ "block": "[color=#ffcc00]█[/color] [color=#fe8019]%s[/color]",
+ "item_ok_time": "[color=#b8bb26]• %s ✔[/color]\t[color=slategray](%sms)[/color]",
+ "item_fail_time": "[color=#fb4934]• %s ✘[/color]\t[color=slategray](%sms)[/color]",
+ "item_info_time": "[color=#8ec07c]• %s [/color]\t[color=slategray](%sms)[/color]",
+ "world_post_import": "[color=tomato]‣ World Post-Import: %s[/color]",
+ "level_post_import": "[color=tomato]‣ Level Post-Import: %s[/color]",
+ "tileset_post_import": "[color=tomato]‣ Tileset Post-Import: %s[/color]",
+ "entity_post_import": "[color=tomato]‣ Entity Post-Import: %s[/color]",
+}
+
+static func nice_print(type: String, message: String, indent: int = 0) -> void:
+ if PRINT_SNIPPET.has(type):
+ var snippet: String = PRINT_SNIPPET[type]
+ snippet = snippet.indent(str("\t").repeat(indent))
+ print_rich(snippet % [message])
+ else:
+ print_rich(message)
+
+static func print(type: String, message: String, indent: int = 0) -> void:
+ nice_print(type, message, indent)
+
+static func print_time(type: String, message: String, time: int = -1, indent: int = 0) -> void:
+ if PRINT_SNIPPET.has(type):
+ var snippet: String = PRINT_SNIPPET[type]
+ snippet = snippet.indent(str("\t").repeat(indent))
+ print_rich(snippet % [message, time])
+ else:
+ print_rich(message)
+
+#endregion
+
+#region References
+static var tilesets := {}
+static var tileset_refs := {}
+static var instance_refs := {}
+static var unresolved_refs := []
+static var path_resolvers := []
+
+static func update_instance_reference(iid: String, instance: Variant) -> void:
+ instance_refs[iid] = instance
+
+static func add_tileset_reference(uid: int, atlas: TileSetAtlasSource) -> void:
+ tileset_refs[uid] = atlas
+
+# This is useful for handling entity instances, as they might not exist yet when encountered
+# or be overwritten at a later stage (e.g. post-import) when importing an LDTK level/world.
+static func add_unresolved_reference(
+ object: Variant,
+ property: Variant,
+ node: Variant = object,
+ iid: String = str(object[property])
+) -> void:
+
+ unresolved_refs.append({
+ "object": object,
+ "property": property,
+ "node": node,
+ "iid": iid
+ })
+
+static func handle_references() -> void:
+ resolve_references()
+ clean_references()
+ clean_resolvers()
+
+static func resolve_references() -> void:
+ var count := unresolved_refs.size()
+ if (count == 0 or not options.resolve_entityrefs):
+ if (options.verbose_output): nice_print("item_info", "No references to resolve", 1)
+ return
+ else:
+ if (options.verbose_output): nice_print("item_info", "Resolving %s references" % [count], 1)
+
+ var solved_refcount := 0
+
+ for ref in unresolved_refs:
+ var iid: String = ref.iid
+ var object: Variant = ref.object # Expected: Node, Dict or Array
+ var property: Variant = ref.property # Expected: String or Int
+ var node: Variant = ref.node # Expected: Node, but needs to accept null
+
+ if instance_refs.has(iid):
+ var instance = instance_refs[iid]
+
+ if instance is Node and node is Node:
+ # BUG: When using 'Pack Levels', external references cannot be resolved at import time. (e.g. Level_0 -> Level_1)
+ # Internal references can resolve, but Godot pushes the error: Parameter "common_parent" is null.
+ # Currently it's a choice between a bunch of errors (that suppress other messages), or no resolving.
+ if true: #instance.owner != null and node.owner != null:
+ var path = node.get_path_to(instance)
+ if path:
+ object[property] = path
+ else:
+ nice_print("item_fail", "Cannot resolve ref (out-of-bounds?) '%s' '%s'" % [instance.name, node.name], 1)
+ continue
+ else:
+ object[property] = instance
+
+ solved_refcount += 1
+
+ var leftover_refcount: int = unresolved_refs.size() - solved_refcount
+ if leftover_refcount > 0:
+ nice_print("item_info", "Could not resolve %s references, most likely non-existent entities." % [leftover_refcount], 1)
+
+static func clean_references() -> void:
+ tileset_refs.clear()
+ instance_refs.clear()
+ unresolved_refs.clear()
+
+static func clean_resolvers() -> void:
+ for resolver in path_resolvers:
+ resolver.free()
+ path_resolvers.clear()
+
+#endregion
diff --git a/godot/addons/ldtk-importer/src/util/util.gd.uid b/godot/addons/ldtk-importer/src/util/util.gd.uid
new file mode 100644
index 0000000..074c89d
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/util/util.gd.uid
@@ -0,0 +1 @@
+uid://ddr8yxjsfqgji
diff --git a/godot/addons/ldtk-importer/src/world.gd b/godot/addons/ldtk-importer/src/world.gd
new file mode 100644
index 0000000..1be674c
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/world.gd
@@ -0,0 +1,89 @@
+@tool
+
+const Util = preload("util/util.gd")
+const PostImport = preload("post-import.gd")
+
+static func create_world(
+ name: String,
+ iid: String,
+ levels: Array,
+ base_dir: String
+) -> LDTKWorld:
+
+ Util.timer_start(Util.DebugTime.GENERAL)
+ var world = LDTKWorld.new()
+ world.name = name
+ world.iid = iid
+
+ # Update World_Rect
+ var x1 = world.rect.position.x
+ var x2 = world.rect.end.x
+ var y1 = world.rect.position.y
+ var y2 = world.rect.end.y
+
+ var worldDepths := {}
+
+ for level in levels:
+ level.position = level.world_position
+
+ if Util.options.group_world_layers:
+ var worldDepthLayer: LDTKWorldLayer
+ var z_index: int = level.z_index if (level is not PackedScene) else 0
+ if not z_index in worldDepths:
+ worldDepthLayer = LDTKWorldLayer.new()
+ worldDepthLayer.name = "WorldLayer_" + str(z_index)
+ worldDepthLayer.depth = z_index
+ world.add_child(worldDepthLayer)
+ worldDepthLayer.set_owner(world)
+ worldDepths[z_index] = worldDepthLayer
+ else:
+ worldDepthLayer = worldDepths[z_index]
+ worldDepthLayer.add_child(level)
+ else:
+ world.add_child(level)
+
+ x1 = min(x1, level.position.x)
+ y1 = min(y1, level.position.y)
+ x2 = max(x2, level.position.x + level.size.x)
+ y2 = max(y2, level.position.y + level.size.y)
+
+ # Set owner - this ensures nodes get saved correctly
+ level.set_owner(world)
+ if not (Util.options.pack_levels):
+ Util.recursive_set_owner(level, world)
+
+ # Sort WorldLayers based on depth
+ if not worldDepths.is_empty():
+ var keys = worldDepths.keys()
+ keys.sort_custom(func(a,b): return a < b)
+ for i in range(keys.size()):
+ world.move_child(worldDepths[keys[i]], i)
+
+ world.rect.position = Vector2i(x1, y1)
+ world.rect.end = Vector2i(x2, y2)
+
+ Util.timer_finish("World Created", 1)
+
+ # Post-Import
+ if (Util.options.world_post_import):
+ world = PostImport.run_world_post_import(world, Util.options.world_post_import)
+
+ return world
+
+static func create_multi_world(
+ name: String,
+ iid: String,
+ worlds: Array[LDTKWorld]
+) -> LDTKWorld:
+
+ var multi_world = LDTKWorld.new()
+ multi_world.name = name
+ multi_world.iid = iid
+
+ worlds.sort_custom(func(a, b): return a.depth < b.depth)
+
+ for world in worlds:
+ multi_world.add_child(world)
+ Util.recursive_set_owner(world, multi_world)
+
+ return multi_world
diff --git a/godot/addons/ldtk-importer/src/world.gd.uid b/godot/addons/ldtk-importer/src/world.gd.uid
new file mode 100644
index 0000000..6f83d3a
--- /dev/null
+++ b/godot/addons/ldtk-importer/src/world.gd.uid
@@ -0,0 +1 @@
+uid://dxg0dj75iw7w8
diff --git a/assets/pipeline.sh b/godot/assets/pipeline.sh
similarity index 100%
rename from assets/pipeline.sh
rename to godot/assets/pipeline.sh
diff --git a/assets/processed/tilesets/sidewalk_128x128.png b/godot/assets/processed/tilesets/sidewalk_128x128.png
similarity index 100%
rename from assets/processed/tilesets/sidewalk_128x128.png
rename to godot/assets/processed/tilesets/sidewalk_128x128.png
diff --git a/godot/assets/processed/tilesets/sidewalk_128x128.png.import b/godot/assets/processed/tilesets/sidewalk_128x128.png.import
new file mode 100644
index 0000000..8df3da8
--- /dev/null
+++ b/godot/assets/processed/tilesets/sidewalk_128x128.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d0k22dcuuv8uy"
+path="res://.godot/imported/sidewalk_128x128.png-cde506a7daa48b45baa91d26762bc08d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/processed/tilesets/sidewalk_128x128.png"
+dest_files=["res://.godot/imported/sidewalk_128x128.png-cde506a7daa48b45baa91d26762bc08d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/processed/tilesets/sidewalk_512x512.png b/godot/assets/processed/tilesets/sidewalk_512x512.png
similarity index 100%
rename from assets/processed/tilesets/sidewalk_512x512.png
rename to godot/assets/processed/tilesets/sidewalk_512x512.png
diff --git a/godot/assets/processed/tilesets/sidewalk_512x512.png.import b/godot/assets/processed/tilesets/sidewalk_512x512.png.import
new file mode 100644
index 0000000..9a81528
--- /dev/null
+++ b/godot/assets/processed/tilesets/sidewalk_512x512.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8sqa7vj6amn2"
+path="res://.godot/imported/sidewalk_512x512.png-14776adf2872cddafc407ac4b59f47ee.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/processed/tilesets/sidewalk_512x512.png"
+dest_files=["res://.godot/imported/sidewalk_512x512.png-14776adf2872cddafc407ac4b59f47ee.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/raw/residential.png b/godot/assets/raw/residential.png
similarity index 100%
rename from assets/raw/residential.png
rename to godot/assets/raw/residential.png
diff --git a/godot/assets/raw/residential.png.import b/godot/assets/raw/residential.png.import
new file mode 100644
index 0000000..c9bc454
--- /dev/null
+++ b/godot/assets/raw/residential.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dm00rxmhhswnc"
+path="res://.godot/imported/residential.png-262e0878b74a2d5b43ab285d83624e92.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/raw/residential.png"
+dest_files=["res://.godot/imported/residential.png-262e0878b74a2d5b43ab285d83624e92.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/raw/sidewalk.png b/godot/assets/raw/sidewalk.png
similarity index 100%
rename from assets/raw/sidewalk.png
rename to godot/assets/raw/sidewalk.png
diff --git a/godot/assets/raw/sidewalk.png.import b/godot/assets/raw/sidewalk.png.import
new file mode 100644
index 0000000..200b3d0
--- /dev/null
+++ b/godot/assets/raw/sidewalk.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b5gsu4gxtaxrw"
+path="res://.godot/imported/sidewalk.png-2c383e30cad907bc6b4c98e9b24edac2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/raw/sidewalk.png"
+dest_files=["res://.godot/imported/sidewalk.png-2c383e30cad907bc6b4c98e9b24edac2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/godot/car.gd b/godot/car.gd
index 4787826..10f825e 100644
--- a/godot/car.gd
+++ b/godot/car.gd
@@ -3,7 +3,7 @@ extends RigidBody2D
@onready var cam = $Camera2D
# zoom range: from close-in to far-out
-var min_zoom = Vector2(1.0, 1.0)
+var min_zoom = Vector2(0.5, 0.5)
var max_zoom = Vector2(0.2, 0.2)
var max_speed = 5000.0 # max expected speed
diff --git a/godot/car.tscn b/godot/car.tscn
index 7dbd2fb..f481f3f 100644
--- a/godot/car.tscn
+++ b/godot/car.tscn
@@ -3,7 +3,7 @@
[ext_resource type="Texture2D" uid="uid://cds01w4trqeei" path="res://assets/police_car.png" id="1_7822p"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_37kl0"]
-size = Vector2(631, 1429)
+size = Vector2(604, 1300)
[node name="Car" type="Car"]
mass = 1300.0
@@ -11,9 +11,9 @@ inertia = 5e+07
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0.5, -0.5)
-scale = Vector2(0.5, 0.5)
+scale = Vector2(0.25, 0.25)
shape = SubResource("RectangleShape2D_37kl0")
[node name="Sprite2D" type="Sprite2D" parent="."]
-scale = Vector2(0.5, 0.5)
+scale = Vector2(0.25, 0.25)
texture = ExtResource("1_7822p")
diff --git a/godot/game.tscn b/godot/game.tscn
index 0c5b949..54faee4 100644
--- a/godot/game.tscn
+++ b/godot/game.tscn
@@ -1,39 +1,21 @@
-[gd_scene load_steps=5 format=3 uid="uid://7713s03g7nxw"]
+[gd_scene load_steps=3 format=3 uid="uid://7713s03g7nxw"]
[ext_resource type="PackedScene" uid="uid://ukvcgdjxtkqw" path="res://car.tscn" id="1_80nbo"]
-[ext_resource type="PackedScene" uid="uid://drkq82mmv8ofa" path="res://test_track.tscn" id="1_e2o6t"]
-[ext_resource type="Script" uid="uid://bic64vi6lpelp" path="res://car.gd" id="4_7jktm"]
-[ext_resource type="Texture2D" uid="uid://bxwhirvk4jjpl" path="res://assets/textures/curb_1.png" id="4_fc0e3"]
+[ext_resource type="PackedScene" uid="uid://c38ikiqaivhh5" path="res://map.tscn" id="1_feb5d"]
[node name="Node2D" type="Node2D"]
-[node name="Node2D" parent="." instance=ExtResource("1_e2o6t")]
-position = Vector2(1024, -7680)
-rotation = 1.57079
-
-[node name="Node2D2" parent="." instance=ExtResource("1_e2o6t")]
-position = Vector2(1024, -17920)
-rotation = 1.57079
-
-[node name="Node2D3" parent="." instance=ExtResource("1_e2o6t")]
-position = Vector2(1024, -28160)
-rotation = 1.57079
-
-[node name="Curb1" type="Sprite2D" parent="."]
-position = Vector2(-1024, 1022.44)
-rotation = 1.57079
-scale = Vector2(1.99747, 0.147406)
-texture = ExtResource("4_fc0e3")
+[node name="test" parent="." instance=ExtResource("1_feb5d")]
+position = Vector2(-2048, -2560)
[node name="Car" parent="." instance=ExtResource("1_80nbo")]
controlled_by_player = true
-script = ExtResource("4_7jktm")
[node name="Camera2D" type="Camera2D" parent="Car"]
position = Vector2(0, -1)
scale = Vector2(0.1, 0.1)
-zoom = Vector2(0.36, 0.36)
+zoom = Vector2(0.5, 0.5)
[node name="Car2" parent="." instance=ExtResource("1_80nbo")]
-position = Vector2(512, -1536)
-rotation = -1.0472
+position = Vector2(-512, -1536)
+rotation = -1.83259
diff --git a/godot/levels/Level_0.scn b/godot/levels/Level_0.scn
new file mode 100644
index 0000000..d16a660
Binary files /dev/null and b/godot/levels/Level_0.scn differ
diff --git a/godot/map.tscn b/godot/map.tscn
new file mode 100644
index 0000000..8a3b900
--- /dev/null
+++ b/godot/map.tscn
@@ -0,0 +1,5 @@
+[gd_scene load_steps=2 format=3 uid="uid://c38ikiqaivhh5"]
+
+[ext_resource type="PackedScene" uid="uid://bydclumuwqch8" path="res://test.ldtk" id="1_c7s6e"]
+
+[node name="test" instance=ExtResource("1_c7s6e")]
diff --git a/godot/project.godot b/godot/project.godot
index fa4c97a..3187622 100644
--- a/godot/project.godot
+++ b/godot/project.godot
@@ -15,6 +15,10 @@ run/main_scene="uid://7713s03g7nxw"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/ldtk-importer/plugin.cfg")
+
[input]
move_forward={
diff --git a/ldtk/test.ldtk b/godot/test.ldtk
similarity index 99%
rename from ldtk/test.ldtk
rename to godot/test.ldtk
index bcb9e56..daf27b8 100644
--- a/ldtk/test.ldtk
+++ b/godot/test.ldtk
@@ -226,7 +226,7 @@
"__cHei": 4,
"identifier": "Sidewalk_128x128",
"uid": 1,
- "relPath": "../assets/processed/tilesets/sidewalk_128x128.png",
+ "relPath": "assets/processed/tilesets/sidewalk_128x128.png",
"embedAtlas": null,
"pxWid": 128,
"pxHei": 128,
@@ -245,7 +245,7 @@
"__cHei": 1,
"identifier": "Residential",
"uid": 6,
- "relPath": "../assets/raw/residential.png",
+ "relPath": "assets/raw/residential.png",
"embedAtlas": null,
"pxWid": 1024,
"pxHei": 1024,
@@ -292,7 +292,7 @@
"__pxTotalOffsetX": 0,
"__pxTotalOffsetY": 0,
"__tilesetDefUid": 6,
- "__tilesetRelPath": "../assets/raw/residential.png",
+ "__tilesetRelPath": "assets/raw/residential.png",
"iid": "2baa2510-3740-11f0-80fd-75b2b5fa7dd7",
"levelId": 0,
"layerDefUid": 9,
@@ -317,7 +317,7 @@
"__pxTotalOffsetX": 0,
"__pxTotalOffsetY": 0,
"__tilesetDefUid": 1,
- "__tilesetRelPath": "../assets/processed/tilesets/sidewalk_128x128.png",
+ "__tilesetRelPath": "assets/processed/tilesets/sidewalk_128x128.png",
"iid": "c0f40ef0-3740-11f0-80fd-29ce8cc19ef6",
"levelId": 0,
"layerDefUid": 2,
diff --git a/godot/test.ldtk.import b/godot/test.ldtk.import
new file mode 100644
index 0000000..974b54b
--- /dev/null
+++ b/godot/test.ldtk.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="ldtk.import"
+type="PackedScene"
+uid="uid://bydclumuwqch8"
+path="res://.godot/imported/test.ldtk-e10b9543a89b321d04f19d9114d464c5.scn"
+
+[deps]
+
+files=["res:///tilesets/tileset_1024px.res", "res:///tilesets/tileset_32px.res", "res:///levels/Level_0.scn", "res://.godot/imported/test.ldtk-e10b9543a89b321d04f19d9114d464c5.scn"]
+
+source_file="res://test.ldtk"
+dest_files=["res://.godot/imported/test.ldtk-e10b9543a89b321d04f19d9114d464c5.scn", "res:///tilesets/tileset_1024px.res", "res:///tilesets/tileset_32px.res", "res:///levels/Level_0.scn", "res://.godot/imported/test.ldtk-e10b9543a89b321d04f19d9114d464c5.scn"]
+
+[params]
+
+World=""
+group_world_layers=false
+Level=""
+pack_levels=true
+Layer=""
+layers_always_visible=false
+Tileset=""
+tileset_custom_data=false
+integer_grid_tilesets=false
+atlas_texture_type=0
+Entity=""
+resolve_entityrefs=true
+use_entity_placeholders=false
+Post Import=""
+tileset_post_import=""
+entities_post_import=""
+level_post_import=""
+world_post_import=""
+Debug=""
+force_tileset_reimport=false
+verbose_output=false
diff --git a/godot/tilesets/tileset_1024px.res b/godot/tilesets/tileset_1024px.res
new file mode 100644
index 0000000..bca8a6e
Binary files /dev/null and b/godot/tilesets/tileset_1024px.res differ
diff --git a/godot/tilesets/tileset_32px.res b/godot/tilesets/tileset_32px.res
new file mode 100644
index 0000000..cce1cd2
Binary files /dev/null and b/godot/tilesets/tileset_32px.res differ
diff --git a/justfile b/justfile
index a169133..b778cd1 100644
--- a/justfile
+++ b/justfile
@@ -36,10 +36,10 @@ rip-youtube-audio URL:
# Listen to the radio
radio:
- mpv --shuffle radio/
+ mpv --shuffle audio/vehicle_radio
# Run assets pipeline
process-assets:
#!/usr/bin/env sh
- cd assets
+ cd godot/assets
sh pipeline.sh