ldtk tileset import

This commit is contained in:
Asger Juul Brunshøj 2025-06-10 15:37:57 +02:00
parent b6f3ef3452
commit 8e814b9269
82 changed files with 2613 additions and 36 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1 @@
uid://lnqlil4dt4ca

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1 @@
uid://bqk4o57wsdal2

View File

@ -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

View File

@ -0,0 +1 @@
uid://c56fiui27y4ww

View File

@ -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

View File

@ -0,0 +1 @@
uid://05p0rgy0kklx

View File

@ -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

View File

@ -0,0 +1 @@
uid://bwex3re84r6o2

View File

@ -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

View File

@ -0,0 +1 @@
uid://bko7ojwy2jnl

View File

@ -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

View File

@ -0,0 +1 @@
uid://c6umqvu1pttjk

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.a{fill:#fc0;}</style></defs><polygon class="a" points="8.47 12.55 8 12.79 7.53 12.55 2.05 9.8 0.85 10.4 8 14 15.15 10.4 13.95 9.8 8.47 12.55"/><polygon class="a" points="8.47 10.15 8 10.39 7.53 10.15 2.05 7.4 0.85 8 8 11.6 15.15 8 13.95 7.4 8.47 10.15"/><polygon class="a" points="8 2 0.85 5.6 8 9.2 15.15 5.6 8 2"/></svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -0,0 +1 @@
uid://ciskd8jyty7gq

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFCC00;stroke-width:2;stroke-linejoin:round;}
.st1{fill:#FFCC00;stroke:#FFCC00;stroke-miterlimit:10;}
</style>
<path class="st0" d="M3,2h10c0.6,0,1,0.4,1,1v10c0,0.6-0.4,1-1,1H3c-0.6,0-1-0.4-1-1V3C2,2.4,2.4,2,3,2z"/>
<rect x="5" y="4.5" class="st1" width="6" height="1"/>
<rect x="7" y="7.5" class="st1" width="2" height="1"/>
<rect x="5" y="10.5" class="st1" width="6" height="1"/>
<rect x="4.8" y="4.5" class="st1" width="1.5" height="7"/>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@ -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

View File

@ -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="."]

View File

@ -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)

View File

@ -0,0 +1 @@
uid://devv1ueptyklo

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.a{fill:#fc0;}</style></defs><path class="a" d="M8.61,14.27c1.32-1.65,4.33-5.65,4.33-7.9a4.94,4.94,0,1,0-9.88,0h0c0,2.25,3,6.25,4.33,7.9a.78.78,0,0,0,1.1.13ZM8,4.72A1.65,1.65,0,1,1,6.35,6.37,1.65,1.65,0,0,1,8,4.72Z"/></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -0,0 +1 @@
uid://cvvkp8akfp51o

View File

@ -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")

View File

@ -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)

View File

@ -0,0 +1 @@
uid://boyf0tvvercyb

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.a{fill:none;stroke:#fc0;stroke-miterlimit:10;stroke-width:2px;}</style></defs><polygon class="a" points="10.93 2.92 5.07 2.92 2.13 8 5.07 13.08 10.93 13.08 13.87 8 10.93 2.92"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -0,0 +1 @@
uid://46jgmlrl0r3s

View File

@ -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

View File

@ -0,0 +1 @@
uid://w5et03vhdql6

View File

@ -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

View File

@ -0,0 +1 @@
uid://bv4fn2n2ncw12

View File

@ -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

View File

@ -0,0 +1 @@
uid://etx2miclpo5a

View File

@ -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

View File

@ -0,0 +1 @@
uid://10dqdfkkqwa2

View File

@ -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<LocalEnum>"
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<Int>":
return value
"Array<Color>":
return value.map(
func (color):
return Color.from_string(color, Color.MAGENTA)
)
"Array<Point>":
return value.map(
func (point):
return Vector2i(point.cx, point.cy)
)
"Array<Tile>":
return value.map(
func(tile) -> AtlasTexture:
return __parse_tile(tile)
)
"Array<EntityRef>":
hitUnresolved = true
return value.map(
func (ref) -> String:
return ref.entityIid
)
"Array<LocalEnum>":
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<Tile> '%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

View File

@ -0,0 +1 @@
uid://dp7qwjbyo1kx0

View File

@ -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

View File

@ -0,0 +1 @@
uid://cm8kt5fyu4max

View File

@ -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

View File

@ -0,0 +1 @@
uid://oc513xtwqvf1

View File

@ -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

View File

@ -0,0 +1 @@
uid://bp3rb0p6dsayi

View File

@ -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

View File

@ -0,0 +1 @@
uid://bynwlcmpnsjit

View File

@ -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

View File

@ -0,0 +1 @@
uid://ddr8yxjsfqgji

View File

@ -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

View File

@ -0,0 +1 @@
uid://dxg0dj75iw7w8

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 523 KiB

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

BIN
godot/levels/Level_0.scn Normal file

Binary file not shown.

5
godot/map.tscn Normal file
View File

@ -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")]

View File

@ -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={

View File

@ -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,

37
godot/test.ldtk.import Normal file
View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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