From 8e814b92694f5a4db7001e5da350214634853c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Tue, 10 Jun 2025 15:37:57 +0200 Subject: [PATCH] ldtk tileset import --- docs/src/workstreams.md | 7 +- godot/addons/ldtk-importer/README.md | 5 + godot/addons/ldtk-importer/ldtk-importer.gd | 311 +++++++++++++++++ .../addons/ldtk-importer/ldtk-importer.gd.uid | 1 + godot/addons/ldtk-importer/plugin.cfg | 7 + godot/addons/ldtk-importer/plugin.gd | 19 + godot/addons/ldtk-importer/plugin.gd.uid | 1 + .../post-import/entity-template.gd | 15 + .../post-import/entity-template.gd.uid | 1 + .../post-import/level-template.gd | 8 + .../post-import/level-template.gd.uid | 1 + .../post-import/tileset-template.gd | 10 + .../post-import/tileset-template.gd.uid | 1 + .../post-import/world-template.gd | 8 + .../post-import/world-template.gd.uid | 1 + .../src/components/ldtk-entity-layer.gd | 8 + .../src/components/ldtk-entity-layer.gd.uid | 1 + .../src/components/ldtk-entity-layer.svg | 1 + .../components/ldtk-entity-layer.svg.import | 37 ++ .../src/components/ldtk-entity-layer.tscn | 6 + .../src/components/ldtk-entity.gd | 96 +++++ .../src/components/ldtk-entity.gd.uid | 1 + .../src/components/ldtk-entity.svg | 14 + .../src/components/ldtk-entity.svg.import | 37 ++ .../src/components/ldtk-entity.tscn | 9 + .../src/components/ldtk-level.gd | 18 + .../src/components/ldtk-level.gd.uid | 1 + .../src/components/ldtk-level.svg | 1 + .../src/components/ldtk-level.svg.import | 37 ++ .../src/components/ldtk-level.tscn | 6 + .../src/components/ldtk-world-layer.gd | 9 + .../src/components/ldtk-world-layer.gd.uid | 1 + .../src/components/ldtk-world-layer.tscn | 6 + .../src/components/ldtk-world.gd | 22 ++ .../src/components/ldtk-world.gd.uid | 1 + .../src/components/ldtk-world.svg | 1 + .../src/components/ldtk-world.svg.import | 37 ++ .../src/components/ldtk-world.tscn | 6 + godot/addons/ldtk-importer/src/layer.gd | 291 +++++++++++++++ godot/addons/ldtk-importer/src/layer.gd.uid | 1 + godot/addons/ldtk-importer/src/level.gd | 125 +++++++ godot/addons/ldtk-importer/src/level.gd.uid | 1 + godot/addons/ldtk-importer/src/post-import.gd | 60 ++++ .../ldtk-importer/src/post-import.gd.uid | 1 + godot/addons/ldtk-importer/src/tileset.gd | 330 ++++++++++++++++++ godot/addons/ldtk-importer/src/tileset.gd.uid | 1 + .../ldtk-importer/src/util/definition_util.gd | 108 ++++++ .../src/util/definition_util.gd.uid | 1 + .../ldtk-importer/src/util/field-util.gd | 126 +++++++ .../ldtk-importer/src/util/field-util.gd.uid | 1 + .../ldtk-importer/src/util/layer-util.gd | 68 ++++ .../ldtk-importer/src/util/layer-util.gd.uid | 1 + .../ldtk-importer/src/util/level-util.gd | 54 +++ .../ldtk-importer/src/util/level-util.gd.uid | 1 + .../ldtk-importer/src/util/tile-util.gd | 126 +++++++ .../ldtk-importer/src/util/tile-util.gd.uid | 1 + .../ldtk-importer/src/util/time-util.gd | 48 +++ .../ldtk-importer/src/util/time-util.gd.uid | 1 + godot/addons/ldtk-importer/src/util/util.gd | 226 ++++++++++++ .../addons/ldtk-importer/src/util/util.gd.uid | 1 + godot/addons/ldtk-importer/src/world.gd | 89 +++++ godot/addons/ldtk-importer/src/world.gd.uid | 1 + {assets => godot/assets}/pipeline.sh | 0 .../processed/tilesets/sidewalk_128x128.png | Bin .../tilesets/sidewalk_128x128.png.import | 34 ++ .../processed/tilesets/sidewalk_512x512.png | Bin .../tilesets/sidewalk_512x512.png.import | 34 ++ {assets => godot/assets}/raw/residential.png | Bin godot/assets/raw/residential.png.import | 34 ++ {assets => godot/assets}/raw/sidewalk.png | Bin godot/assets/raw/sidewalk.png.import | 34 ++ godot/car.gd | 2 +- godot/car.tscn | 6 +- godot/game.tscn | 32 +- godot/levels/Level_0.scn | Bin 0 -> 107113 bytes godot/map.tscn | 5 + godot/project.godot | 4 + {ldtk => godot}/test.ldtk | 8 +- godot/test.ldtk.import | 37 ++ godot/tilesets/tileset_1024px.res | Bin 0 -> 822 bytes godot/tilesets/tileset_32px.res | Bin 0 -> 1550 bytes justfile | 4 +- 82 files changed, 2613 insertions(+), 36 deletions(-) create mode 100644 godot/addons/ldtk-importer/README.md create mode 100644 godot/addons/ldtk-importer/ldtk-importer.gd create mode 100644 godot/addons/ldtk-importer/ldtk-importer.gd.uid create mode 100644 godot/addons/ldtk-importer/plugin.cfg create mode 100644 godot/addons/ldtk-importer/plugin.gd create mode 100644 godot/addons/ldtk-importer/plugin.gd.uid create mode 100644 godot/addons/ldtk-importer/post-import/entity-template.gd create mode 100644 godot/addons/ldtk-importer/post-import/entity-template.gd.uid create mode 100644 godot/addons/ldtk-importer/post-import/level-template.gd create mode 100644 godot/addons/ldtk-importer/post-import/level-template.gd.uid create mode 100644 godot/addons/ldtk-importer/post-import/tileset-template.gd create mode 100644 godot/addons/ldtk-importer/post-import/tileset-template.gd.uid create mode 100644 godot/addons/ldtk-importer/post-import/world-template.gd create mode 100644 godot/addons/ldtk-importer/post-import/world-template.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity-layer.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity-layer.svg.import create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity-layer.tscn create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity.gd create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity.svg create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity.svg.import create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-entity.tscn create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-level.gd create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-level.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-level.svg create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-level.svg.import create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-level.tscn create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world-layer.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world-layer.tscn create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world.gd create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world.gd.uid create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world.svg create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world.svg.import create mode 100644 godot/addons/ldtk-importer/src/components/ldtk-world.tscn create mode 100644 godot/addons/ldtk-importer/src/layer.gd create mode 100644 godot/addons/ldtk-importer/src/layer.gd.uid create mode 100644 godot/addons/ldtk-importer/src/level.gd create mode 100644 godot/addons/ldtk-importer/src/level.gd.uid create mode 100644 godot/addons/ldtk-importer/src/post-import.gd create mode 100644 godot/addons/ldtk-importer/src/post-import.gd.uid create mode 100644 godot/addons/ldtk-importer/src/tileset.gd create mode 100644 godot/addons/ldtk-importer/src/tileset.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/definition_util.gd create mode 100644 godot/addons/ldtk-importer/src/util/definition_util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/field-util.gd create mode 100644 godot/addons/ldtk-importer/src/util/field-util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/layer-util.gd create mode 100644 godot/addons/ldtk-importer/src/util/layer-util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/level-util.gd create mode 100644 godot/addons/ldtk-importer/src/util/level-util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/tile-util.gd create mode 100644 godot/addons/ldtk-importer/src/util/tile-util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/time-util.gd create mode 100644 godot/addons/ldtk-importer/src/util/time-util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/util/util.gd create mode 100644 godot/addons/ldtk-importer/src/util/util.gd.uid create mode 100644 godot/addons/ldtk-importer/src/world.gd create mode 100644 godot/addons/ldtk-importer/src/world.gd.uid rename {assets => godot/assets}/pipeline.sh (100%) rename {assets => godot/assets}/processed/tilesets/sidewalk_128x128.png (100%) create mode 100644 godot/assets/processed/tilesets/sidewalk_128x128.png.import rename {assets => godot/assets}/processed/tilesets/sidewalk_512x512.png (100%) create mode 100644 godot/assets/processed/tilesets/sidewalk_512x512.png.import rename {assets => godot/assets}/raw/residential.png (100%) create mode 100644 godot/assets/raw/residential.png.import rename {assets => godot/assets}/raw/sidewalk.png (100%) create mode 100644 godot/assets/raw/sidewalk.png.import create mode 100644 godot/levels/Level_0.scn create mode 100644 godot/map.tscn rename {ldtk => godot}/test.ldtk (99%) create mode 100644 godot/test.ldtk.import create mode 100644 godot/tilesets/tileset_1024px.res create mode 100644 godot/tilesets/tileset_32px.res 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 0000000000000000000000000000000000000000..d16a66060ee3fea4fb7e555573997bb70a4113e6 GIT binary patch literal 107113 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 hH`NL;vq*^sp4j>PtWvOzxc1hXM- z^8&LW;ouEsL)_^DW<%`q1+yXQ{lIL9`~AUekcptx!=Uhl=nVvmgF+KD{|*v|_%RqN z4pJ2YWrI|Og4q!Fhk@CUd=w65gH(gcACNg9)u56B#D=7qD5x5cOf-}Y(h&n?gM1JR zW<%mA4$OwcV?3A*F$Z*#8AueQmysa~+!g|{nZRs_8fGvXBnn#pj8MZ076;h`T7L); z2l3d!;t+pvK-nO7azfc4HC#|OC=|KDY)IJhfY}gxdBJRm`}x3ZkWSF~V<5XAX+Quh z4sn|xm<=)!wEhR82Bbq6EDlLmB49QoUPQrch&f_lHbjj$lnpXT0?Y=v9@PFpm?;Go z2ZbD{{R0sPsgePUL(+^am<wwu1y}DpF#4mbaHpCo# zC>!Kv10=Q~lnn}VBPbi>I%6mslzY>-_(U^a*a+J6BG2S}Lv zLB&D7@dvX(z6F&(AT^M56$lmwsQ{Hf5OI*tg2CbtH-|vkAR9uVY>(NOAiWSZ>|k+-`#HdDkSU<{4@3>fmt0_Rh+nwD zY=~cYz-&ld^MctRpMb`nL3$za$PX5WumzxOkPU)hHY6^Dplp!1FqjQuf!6CM z3Koa3#lUQc`^CX*kSjs!pF!#&VIm0@hom1VC>!K^X(YA`m<{oZER+pWBL`)Jd?OEL zL(EYCvmtI)1hXOGrUYg~+@K6*L)5E)*${D6C>!K6H82~ZR~^iTn5h9~L)@^>PISO*NI9kpW<%my4~eZ0W<%5%fY}iL8iLsn^No<$#$YzY{U%U0C`?Sj zY)Bd~1G6FSF^952{;~kGL8?Lbe}Uox;vOroIK*CSFdO0?8!#JUrY)Ea3USc*4@57> zOnb06B>gym*^uyY1hXM&zzNKTsBs3fA#QVlvOz9#1+yV;a09a;dfmZnh#Ne>Y)II8 zg4vMp@dC3U=6i$LAeVvmpMu;8Nt3=%agY!Eplp!6{$Mu5oB%Kz5+;FQHbid_lnqKL z!C*GTogrX0#I8^<8=^i8%!Zf~j>L{YVn>465cN@DHbi|im<@6n===?k`yp{13l@ij zZ5)^l@ozks4N(uuVGy5y+$7Hc%IP2(5RVPS0AmRTh#HV@SQ$Vw<_rvZ`6;RK$@!&u zB@Cbd0%4F8NE1k^m_dO7G(^F`@E;05vLMqyTowk1H$Z%lp&;u(CV{1E?tm8r}vigM_Jq&>*#345(^BuHs+-bqyhEKyKk?U|>kj&&xyd1V|Dj z1yT-@f_MTZ3eo`bT6{rbNk%b)97vo2V!tp014C*`W=UdFPHKE+UU5lcUUDj`5|H&} VsYS(^`FZdF19=nV^`PJ&X8?ZG?EwG) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bca8a6e9130de0ad459a5c60dea1c32fd371892f GIT binary patch literal 822 zcmWFv4svFI0u}}a1`uXrU|`^2U|hRoSd4M%D{&#mzS8E3bwr@wW6f72*S=yEK1MJ zD`wzkU|=XtEl4a%EXmBzV-R6rV1O!%FG@|%%+HH2&a6rWi3rw#^U@hW z9y72q&^KVQ$Vy)yng|?AauSP! zA<>sso|&D>kOET4z`!8Jz`%f}I6l4DDAknVK2!xL{xOXK2RX>GtPBj`Kn_BGAXD4CkbRLKuSPPfLQ}G5|m>AieIz= literal 0 HcmV?d00001 diff --git a/godot/tilesets/tileset_32px.res b/godot/tilesets/tileset_32px.res new file mode 100644 index 0000000000000000000000000000000000000000..cce1cd2b2287176ba71f0e54c343d256e2b9c4e5 GIT binary patch literal 1550 zcmWFv4svFI0u}}a1`uXrU|`^2U|sD%kdt)QXbQA_zM-u_!$= zub6?Gfq|howIH!5u_QA;k3ocifdQ&4z9=<4Ge0lBII}7hEM8ii8V{2zNK8q|%u8ng zdCb7dK;M9Ymw|x+%+fDTF3Kz@0ZSRerNAs?DI>TPn1w8543`44kfjW)4B>Ve>Kh ztc>Ay8Kc_;lQIM;Le^yj5`fx;rpp*E1!lo?v4f%;5VYW zyNIh=EHz9F3=E143=H7RWTmeU&8?0lIf=!=kbIVslTuO4zzfRL3=9lnn2O_*(~FFZ z7n7yFz0OSAXfrS{AV&s> O4e}T)LSP1hY61Yc3I?_S literal 0 HcmV?d00001 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