--- [LDtk](https://ldtk.io/) level importer for Lua and drawing using LÖVE. -- Support most LDtk features, and allow easy usage in LÖVE projects. -- In particular, this mainly focus only on features and values that are useful for showing the final level - this does not try, for example, to expose -- every internal identfiers or intermediates values that are only relevant for editing. -- -- Currently up-to-date with LDtk 1.1.3. -- -- Every unit is in pixel in the API unless written otherwise. -- Colors are reprsented as a table `{r,g,b}` where `r`,`b`,`g` in [0-1]. -- -- This modules returns a single function, @{LDtk}(path). -- -- This modules requires [json.lua](https://github.com/rxi/json.lua); a copy of it is included with ubiquitousse in the `lib` directory for simplicity. -- This module will first try to load a global module named `json` - so if you use the same json module in your project ubiquitousse will reuse it. -- If it doesn't find it, it will then try to load the copy included with ubiquitousse. -- -- Optionally requires LÖVE `love.graphics` (drawing Image, SpriteBatch, Quad), for drawing only. -- -- @module ldtk -- @require love -- @usage -- local ldtk = require("ubiquitousse.ldtk") -- -- -- load ldtk project file -- local project = ldtk("example.ldtk") -- -- -- can define callbacks when loading: for example to setup entities defined in LDtk -- local callbacks = { -- onAddEntity = function(entity) -- -- handle entity... -- end -- } -- -- -- load every level, with callbacks -- for _, lvl in ipairs(project.levels) do lvl:load(callbacks) end -- -- function love.draw() -- -- draw every level -- for _, lvl in ipairs(project.levels) do lvl:draw() end -- end -- TODO: give associated tile & color with enum values, also give enum info -- TODO: handle nineSliceBorders when drawing entities -- TODO: Once stable in LDtk: handle parallax when drawing layers, multiple worlds per file -- LÖVE wrappers/placeholder let lg = (love or {}).graphics let newQuad if lg then newQuad = lg.newQuad else newQuad = (x, y, w , h, image) return { x, y, w, h } end end let cache -- json helpers let json_decode do let r, json = pcall(require, "json") if not r then json = require((...):gsub("ldtk%.ldtk$", "lib.json")) end json_decode = json.decode end let readJson = (file) let f = assert(io.open(file, "r")) local t = json_decode(f:read("*a")) f:close() return t end -- color helpers let parseColor = (str) local r, g, b = str:match("^#(..)(..)(..)") r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16) return { r/255, g/255, b/255 } end let white = {1,1,1} --- tileset rectangle helpers let makeTilesetRect = (tilesetRect, project) local tileset = cache.tileset(project._tilesetData[tilesetRect.tilesetUid]) local quad = tileset:_newQuad(tilesetRect.x, tilesetRect.y, tilesetRect.w, tilesetRect.h) return { tileset = tileset, quad = quad } end --- returns a lua table from some fieldInstances let toLua = (type, val, parent_entity) if val == nil then return val end if type:match("^Array%<") then local itype = type:match("^Array%<(.*)%>$") for i, v in ipairs(val) do val[i] = toLua(itype, v, parent_entity) end elseif type == "Color" then return parseColor(val) elseif type == "Point" then assert(parent_entity, "AFAIK, it's not possible to have a Point field in something that's not an entity") return { x = val.cx * parent_entity.layer.gridSize, y = val.cy * parent_entity.layer.gridSize } elseif type == "Tile" then assert(parent_entity, "AFAIK, it's not possible to have a Tile field in something that's not an entity") return makeTilesetRect(val, parent_entity.layer.level.project) elseif type == "EntityRef" then assert(parent_entity, "AFAIK, it's not possible to have an EntityRef field in something that's not an entity") local entityRef = setmetatable({ level = parent_entity.layer.level.project.levels[val.levelIid], layerIid = val.layerIid, entityIid = val.entityIid, entity = nil, }, { __index = :(k) if @level.loaded then if k == "entity" then @entity = @level.layers[@layerIid].entities[@entityIid] return @entity end end return nil end }) return entityRef end return val end let getFields = (f, parent_entity) local t = {} for _, v in ipairs(f) do t[v.__identifier] = toLua(v.__type, v.__value, parent_entity) end return t end let tileset_mt --- cached values let make_cache = (new_fn) return setmetatable({}, { __mode = "v", __call = (cache, id) if not cache[id] then cache[id] = new_fn(id) end return cache[id] end }) end cache = { tileset = make_cache((tilesetDef) return tileset_mt._init(tilesetDef) end), image = make_cache((path) if lg then return lg.newImage(path) else return path end end), } --- Tileset object. -- Stores the image associated with the tileset; can be shared among several layers and levels. -- @type Tileset tileset_mt = { _newQuad = :(x, y, width, height) return newQuad(x, y, width, height, @image) end, _getTileQuad = :(tileid, x, y, size) if not @_tileQuads[tileid] then @_tileQuads[tileid] = @_newQuad(x, y, size, size) end return @_tileQuads[tileid] end, _init = (tilesetDef) assert(not tilesetDef.embedAtlas, "cannot load a tileset that use an internal LDtk atlas image, please use external tileset images") assert(tilesetDef.path, "cannot load a tileset that has no image associated") local t = { --- The tileset LÖVE image object. -- If LÖVE is not available, this is the path to the image (string). image = cache.image(tilesetDef.path), --- Tags associated with the tileset: can be used either as a list of tags or a map of activated tags tags[name] == true. -- @ftype {"tag",["tag"]=true,...} tags = tilesetDef.tags, _tileQuads = {} } return setmetatable(t, tileset_mt) end } tileset_mt.__index = tileset_mt --- Layer object. -- -- Part of a @{Level}. -- -- @type Layer let layer_mt = { --- Draw the current layer. -- -- Assumes we are currently in level coordinates (i.e. level top-left is at 0,0). -- You can specify an offset if your level top-left coordinate is not at 0,0 (or to produce other effects). -- @number[opt=0] x offset X position to draw the layer at -- @number[opt=0] y offset Y position to draw the layer at -- @require love draw = :(x=0, y=0) if @visible then lg.push() lg.translate(x + @x, y + @y) if @spritebatch then lg.setColor(1, 1, 1, @opacity) lg.draw(@spritebatch) elseif @intTiles then for _, t in ipairs(@intTiles) do lg.setColor(t.color) lg.rectangle("fill", t.x, t.y, t.layer.gridSize, t.layer.gridSize) end elseif @entities then for _, e in ipairs(@entities) do if e.draw then e:draw() end end end lg.pop() end end, _unloadCallbacks = :(callbacks) local onRemoveTile = callbacks.onRemoveTile if @tiles and onRemoveTile then for _, t in ipairs(@tiles) do onRemoveTile(t) end end local onRemoveIntTile = callbacks.onRemoveIntTile if @intTiles and onRemoveIntTile then for _, t in ipairs(@intTiles) do onRemoveIntTile(t) end end local onRemoveEntity = callbacks.onRemoveEntity if @entities and onRemoveEntity then for _, e in ipairs(@entities) do onRemoveEntity(e) end end end, _init = (layer, level, order, callbacks) local layerDef = level.project._layerDef[layer.layerDefUid] let gridSize = layer.__gridSize let t = { --- `Level` this layer belongs to. -- @ftype Level level = level, --- Unique instance identifier for this layer. -- @ftype string iid = layer.iid, --- The layer name. -- @ftype string identifier = layer.__identifier, --- Type of layer: IntGrid, Entities, Tiles or AutoLayer. -- @ftype string type = layer.__type, --- Whether the layer is visible or not. -- @ftype boolean visible = layer.visible, --- The layer opacity (0-1). -- @ftype number opacity = layer.opacity, --- The layer order: smaller order means it is on top. -- @ftype number order = order, --- X position of the layer relative to the level. -- @ftype number x = layer.__pxTotalOffsetX, --- Y position of the layer relative to the level. -- @ftype number y = layer.__pxTotalOffsetY, --- Size of the grid on this layer. -- @ftype number gridSize = gridSize, --- Width of the layer, in grid units. -- @ftype number gridWidth = layer.__cWid, --- Height of the layer, in grid units. -- @ftype number gridHeight = layer.__cHei, --- Parallax horizontal factor (from -1 to 1, defaults to 0) which affects the scrolling speed of this layer, creating a fake 3D (parallax) effect. -- @ftype number parallaxFactorX = layerDef.parallaxFactorX, --- Parallax vertical factor (from -1 to 1, defaults to 0) which affects the scrolling speed of this layer, creating a fake 3D (parallax) effect. -- @ftype number parallaxFactorY = layerDef.parallaxFactorY, --- If true, a layer with a parallax factor will also be scaled up/down accordingly. -- @ftype boolean parallaxScaling = layerDef.parallaxScaling, --- _(Entities layer only)_ List of `Entity` in the layer. -- Each entity in the list is also bound to its IID in this table, so if `ent = entities[1]`, you can also find it at `entities[ent.iid]`. -- @ftype {Entity,...} entities = nil, --- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ List of `Tile`s in the layer. -- @ftype {Tile,...} -- @ftype nil if not applicable tiles = nil, --- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ `Tileset` object associated with the layer. -- @ftype Tileset -- @ftype nil if not applicable tileset = nil, --- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ [LÖVE SpriteBatch](https://love2d.org/wiki/SpriteBatch) containing the layer. -- @ftype SpriteBatch -- @ftype nil if LÖVE not available. -- @require love spritebatch = nil, --- _(IntGrid without AutoLayer rules layer only)_ list of `IntTile`s in the layer. -- @ftype {IntTile,...} -- @ftype nil if not applicable intTiles = nil, } -- Layers with an associated tileset (otherwise ignore as there is nothing to draw) (Tiles, AutoLayer & IntGrid with AutoLayer rules) if layer.__tilesetDefUid then t.tiles = {} local tilesetData = level.project._tilesetData[layer.__tilesetDefUid] t.tileset = cache.tileset(tilesetData) local tiles = layer.__type == "Tiles" and layer.gridTiles or layer.autoLayerTiles local onAddTile = callbacks.onAddTile if lg then t.spritebatch = lg.newSpriteBatch(t.tileset.image) end for _, tl in ipairs(tiles) do let quad = t.tileset:_getTileQuad(tl.t, tl.src[1], tl.src[2], gridSize) let sx, sy = 1, 1 let x, y = tl.px[1], tl.px[2] --- Tile object. -- -- This represent the tiles from a Tiles, AutoLayer or IntGrid with AutoLayer rules layer. -- -- Can be retrived from the `Layer.tiles` list or `onAddTile` level load callback. -- -- @type Tile let tile = { --- `Layer` the tile belongs to. -- @ftype Layer layer = t, --- X position of the tile relative to the layer, in pixels. -- @ftype number x = x, --- Y position of the tile relative to the layer, in pixels. -- @ftype number y = y, --- Whether the tile is flipped horizontally. -- @ftype boolean flipX = false, --- Whether the tile is flipped vertically. -- @ftype boolean flipY = false, --- Tags associated with the tile: can be used either as a list of tags or a map of activated tags tags[name] == true. -- @ftype {"tag",["tag"]=true,...} tags = tilesetData[tl.t].enumTags, --- Custom data associated with the tile, if any. -- @ftype string data = tilesetData[tl.t].data, --- Quad associated with the tile (relative to the layer's tileset). -- @ftype LÖVE Quad if LÖVE is available -- @ftype table { x, y, width, height } if LÖVE not available quad = quad } if tl.f == 1 or tl.f == 3 then sx = -1 x += gridSize tile.flipX = true end if tl.f == 2 or tl.f == 3 then sy = -1 y += gridSize tile.flipY = true end if t.spritebatch then t.spritebatch:add(quad, x, y, 0, sx, sy) end table.insert(t.tiles, tile) if onAddTile then onAddTile(tile) end end -- IntGrid elseif layer.__type == "IntGrid" then t.intTiles = {} local onAddIntTile = callbacks.onAddIntTile local values = layerDef.intGridValues for i, tl in ipairs(layer.intGridCsv) do if tl > 0 then let y = math.floor((i-1) / t.gridWidth) * gridSize let x = ((i-1) % t.gridWidth) * gridSize --- IntTile object. -- -- This represent the tiles from a IntGrid without AutoLayer rules layer. -- -- Can be retrived from the @{intTiles} list or `onAddIntTile` level load callback. -- -- @type IntTile let tile = { --- `Layer` the IntTile belongs to. -- @ftype Layer layer = t, --- X position of the IntTile relative to the layer, in pixels. -- @ftype number x = x, --- Y position of the IntTile relative to the layer, in pixels. -- @ftype number y = y, --- Name of the IntTile. -- @ftype string identifier = values[tl].identifier, --- Integer value of the IntTile. -- @ftype number value = tl, --- Color of the IntTile. -- @ftype table {r,g,b} with r,g,b in [0-1] color = values[tl].color } table.insert(t.intTiles, tile) if onAddIntTile then onAddIntTile(tile) end end end end -- Entities layers if layer.__type == "Entities" then t.entities = {} local onAddEntity = callbacks.onAddEntity for _, e in ipairs(layer.entityInstances) do let entityDef = level.project._entityData[e.defUid] --- Entity object. -- -- This represent an entity from an Entities layer. -- -- Can be retrived from the @{entities} list or `onAddEntity` level load callback. -- -- @type Entity let entity = { --- `Layer` this entity belongs to. -- @ftype Layer layer = t, --- Unique instance identifier for this entity. -- @ftype string iid = e.iid, --- The entity name. -- @ftype string identifier = e.__identifier, --- X position of the entity relative to the layer, in pixels. -- @ftype number x = e.px[1], --- Y position of the entity relative to the layer, in pixels. -- @ftype number y = e.px[2], --- The entity width, in pixels. -- @ftype number width = e.width, --- The entity height, in pixels. -- @ftype number height = e.height, --- Scale factor on x axis relative to original entity size. -- @ftype number sx = e.width / entityDef.width, --- Scale factor on y axis relative to original entity size. -- @ftype number sy = e.height / entityDef.height, --- The entity pivot point x position relative to the entity, in pixels.. -- @ftype number pivotX = e.__pivot[1] * e.width, --- The entity pivot point x position relative to the entity, in pixels.. -- @ftype number pivotY = e.__pivot[2] * e.height, --- Entity color. -- @ftype table {r,g,b} with r,g,b in [0-1] color = parseColor(e.__smartColor), --- Tile associated with the entity, if any. Is a table { tileset = associated tileset object, quad = associated quad }. -- `quad` is a LÖVE Quad if LÖVE is available, otherwise a table `{ x, y, width, height }`. -- @ftype table tile = nil, --- Tags associated with the entity: can be used either as a list of tags or a map of activated tags tags[name] == true. -- @ftype {"tag",["tag"]=true,...} tags = e.__tags, --- Map of `CustomFields` of the entity. -- @ftype CustomFields fields = nil, --- Called for the entity when drawing the associated entity layer (you will likely want to redefine it). -- -- By default, this draws the tile associated with the entity if there is one, or a rectangle around the entity position otherwise, -- assuming we are currently in layer coordinates (i.e. layer top-left is at 0,0). -- @require love draw = :() if @tile then let _, _, w, h = @tile.quad:getViewport() lg.setColor(white) lg.draw(@tile.tileset.image, @tile.quad, @x-@pivotX, @y-@pivotY, 0, @width / w, @height / h) else lg.setColor(@color) lg.rectangle("line", @x-@pivotX, @y-@pivotY, @width, @height) end end } if e.__tile then entity.tile = makeTilesetRect(e.__tile, level.project) end for _, tag in ipairs(entity.tags) do entity.tags[tag] = true end entity.fields = getFields(e.fieldInstances, entity) t.entities[entity.iid] = entity table.insert(t.entities, entity) if onAddEntity then onAddEntity(entity) end end end return setmetatable(t, layer_mt) end } layer_mt.__index = layer_mt --- Level object. -- -- Levels are not automatically loaded in order to not waste ressources if your project is large; so before drawing or operating on a level, you will need to call its @{Level:load} method. -- -- Part of a @{Project}. -- -- @type Level let level_mt = { --- Draw this level. -- Will draw the eventual backgrounds and all the layers in the level. -- -- Assumes we are currently in world coordinates (i.e. world top-left is at 0,0). -- You can specify an offset if your world top-left coordinate is not at 0,0 (or to produce other effects). -- -- The level must be loaded. -- @number[opt=0] x offset X position to draw the level at -- @number[opt=0] y offset Y position to draw the level at -- @require love draw = :(x=0, y=0) assert(@loaded == true, ("level %q not loaded"):format(@identifier)) lg.push() lg.translate(x + @x, y + @y) @drawBackground() -- layers for _, l in ipairs(@layers) do l:draw() end lg.pop() end, --- Draw this level background. -- -- Assumes we are currently in level coordinates (i.e. level top-left is at 0,0). -- You can specify an offset if your level top-left coordinate is not at 0,0 (or to produce other effects). -- -- The level must be loaded. -- @number[opt=0] x offset X position to draw the background at -- @number[opt=0] y offset Y position to draw the backgroud at -- @require love drawBackground = :(x=0, y=0) assert(@loaded == true, ("level %q not loaded"):format(@identifier)) -- background color lg.setColor(@background.color) lg.rectangle("fill", x, y, @width, @height) -- background image lg.setColor(white) let bgImage = @background.image if bgImage then lg.draw(bgImage.image, bgImage.quad, x + bgImage.x, y + bgImage.y, 0, bgImage.sx, bgImage.sy) end end, --- Load the level. -- Will load every layer in the level and the associated images. -- -- You can optionally specify some callbacks for the loading process: -- -- * `onAddLayer(layer)` will be called for every new layer loaded, with the @{Layer} as sole argument -- * `onAddTile(tile)` will be called for every new tile loaded, with the @{Tile} as sole argument -- * `onAddIntTile(tile)` will be called for every new IntGrid tile loaded, with the @{IntTile} as sole argument -- * `onAddEntity(entity)` will be called for every new entity loaded, with the @{Entity} as sole argument -- -- These callbacks should allow you to capture all the important elements needed to use the level, so you can hopefully -- integrate it into your current game engine easily. -- -- @tparam[opt] table callbacks load = :(callbacks={}) assert(@loaded == false, ("level %q already loaded"):format(@identifier)) if @_json.bgRelPath then let pos = @_json.__bgPos let cropRect = pos.cropRect let image = cache.image(@project._directory..@_json.bgRelPath) @background.image = { image = image, quad = newQuad(cropRect[1], cropRect[2], cropRect[3], cropRect[4], image), x = pos.topLeftPx[1], y = pos.topLeftPx[2], sx = pos.scale[1], sy = pos.scale[1] } end let layerInstances if @_json.externalRelPath then layerInstances = readJson(@project._directory..@_json.externalRelPath).layerInstances else layerInstances = @_json.layerInstances end @layers = {} let onAddLayer = callbacks.onAddLayer for i=#layerInstances, 1, -1 do local layer = layer_mt._init(layerInstances[i], @, i, callbacks) @layers[layer.iid] = layer table.insert(@layers, layer) if onAddLayer then onAddLayer(layer) end end @loaded = true end, --- Unload the level. -- Images loaded by the level will be freed on the next garbage collection cycle. -- -- You can optionally specify some callbacks for the unloading process: -- -- * `onAddLayer(layer)` will be called for every new layer unloaded, with the @{Layer} as sole argument -- * `onAddTile(tile)` will be called for every new tile unloaded, with the @{Tile} as sole argument -- * `onAddIntTile(tile)` will be called for every new IntGrid tile unloaded, with the @{IntTile} as sole argument -- * `onAddEntity(entity)` will be called for every new entity unloaded, with the @{Entity} as sole argument -- -- @tparam[opt] table callbacks unload = :(callbacks={}) assert(@loaded == true, ("level %q not loaded"):format(@identifier)) let onRemoveLayer = callbacks.onRemoveLayer for _, l in ipairs(@layers) do l:_unloadCallbacks(callbacks) if onRemoveLayer then onRemoveLayer(l) end end @loaded = false @background.image = nil @layers = nil end, _init = (level, project) let t = { --- `Project` this level belongs to. -- @ftype Project project = project, --- Whether this level is currently loaded or not. -- @ftype boolean loaded = false, --- Unique instance identifier for this level. -- @ftype string iid = level.iid, --- The level name. -- @ftype string identifier = level.identifier, --- Depth of the level in the world, to properly stack overlapping levels when drawing. Default is 0, greater means above, lower means below. -- @ftype number depth = level.worldDepth, --- The level x position in pixels. -- For Horizontal and Vertical layouts, is always -1. -- @ftype number x = level.worldX, --- The level y position in pixels. -- For Horizontal and Vertical layouts, is always -1. -- @ftype number y = level.worldY, --- The level width. -- @ftype number width = level.pxWid, --- The level height. -- @ftype number height = level.pxHei, --- Map of `CustomFields` of the level (table). -- @ftype CustomFields fields = getFields(level.fieldInstances), --- List of `Layer`s in the level (table). -- Each layer in the list is also bound to its IID in this table, so if `lay = layers[1]`, you can also find it at `layers[lay.iid]`. -- @ftype {Layer,...} layers = nil, --- Level background. -- -- If there is a background image, `background.image` contains a table `{image=image, x=number, y=number, sx=number, sy=number}` -- where `image` is the LÖVE image (or image filepath if LÖVE not available) `x` and `y` are the top-left position, -- and `sx` and `sy` the horizontal and vertical scale factors. -- @field color backrgound color {r,g,b} with r,g,b in [0-1] -- @field image backrgound image information, if any background = { color = parseColor(level.__bgColor), image = nil, }, -- private _json = level, } return setmetatable(t, level_mt) end } level_mt.__index = level_mt --- Project object. -- -- Returned by @{LDtk}. -- -- @type Project let project_mt = { _init = (project, directory) assert(project.jsonVersion:match("^1%.1%."), "the map was made with LDtk version %s but the importer is made for 1.1.3":format(project.jsonVersion)) let t = { --- List of `Level`s in this project. -- Each level in the list is also bound to its IID in this table, so if `lvl = levels[1]`, you can also find it at `levels[lvl.iid]`. -- @ftype {Level,...} levels = nil, -- private _directory = directory, _layerDef = nil, _tilesetData = nil, _entityData = nil, } t.levels = [ for _, lvl in ipairs(project.levels) do local level = level_mt._init(lvl, t) @[lvl.iid] = level push level end ] t._tilesetData = [ for _, ts in ipairs(project.defs.tilesets) do @[ts.uid] = { tags = ts.tags } if ts.relPath then @[ts.uid].path = directory..ts.relPath elseif ts.embedAtlas then @[ts.uid].embedAtlas = true -- will error if game try to use this tileset end for _, tag in ipairs(ts.tags) do @[ts.uid].tags[tag] = true end local tilesetData = @[ts.uid] for gridx=0, ts.__cWid-1 do for gridy=0, ts.__cHei-1 do tilesetData[gridx + gridy * ts.__cWid] = { enumTags = {}, data = nil } end end for _, custom in ipairs(ts.customData) do tilesetData[custom.tileId].data = custom.data end for _, tag in ipairs(ts.enumTags) do local value = tag.enumValueId for _, tileId in ipairs(tag.tileIds) do table.insert(tilesetData[tileId].enumTags, value) tilesetData[tileId].enumTags[value] = true end end end ] t._layerDef = [ for _, lay in ipairs(project.defs.layers) do @[lay.uid] = { intGridValues = nil, parallaxFactorX = lay.parallaxFactorX, parallaxFactorY = lay.parallaxFactorY, parallaxScaling = lay.parallaxScaling } local layerDef = @[lay.uid] if lay.__type == "IntGrid" then layerDef.intGridValues = [ for _, v in ipairs(lay.intGridValues) do @[v.value] = { color = parseColor(v.color), identifier = v.identifier } end ] end end ] t._entityData = [ for _, ent in ipairs(project.defs.entities) do @[ent.uid] = { width = ent.width, height = ent.height, nineSliceBorders = #ent.nineSliceBorders > 0 and ent.nineSliceBorders or nil } end ] return setmetatable(t, project_mt) end } project_mt.__index = project_mt --- Custom fields: map of each field name to its value. -- -- LDtk allows to defined custom fields in some places (`Entity.fields`, `Level.fields`). This library allows you to access them in a table that -- map each field name to its value `{["fieldName"]=value,...}`. -- -- @type CustomFields --- Type conversion. -- -- Here is how the values are converted to Lua values: -- -- * Integers, Floats are converted into a Lua number. -- * Booleans are converted into a Lua boolean. -- * Strings, Multilines are converted in a Lua string. -- * Enum are converted into a Lua string giving the currently selected enum value. -- * Filepath are converted into a Lua string giving the file path. -- * Arrays are converted into a Lua table with the elements in it as a list. -- * Points are converted into a Lua table with the fields `x` and `y`, in pixels: `{ x=number, y=number }`. -- * Colors are converted into a Lua table with the red, green and blue components in [0-1] as a list: `{r,g,b}`. -- * Tiles are converted into a Lua table { tileset = associated tileset object, quad = associated quad } where `quad` is a LÖVE Quad if LÖVE is available, otherwise a table `{ x, y, width, height }`. -- * EntityRef are converted into a Lua table { level = level, layerIid = layer IID, entityIid = entity IID, entity = see explanation }. If the entity being refernced belongs to another level and this level is not loaded, `entity` will be nil; otherwise (same level or the other level is also loaded), it will contain the entity. -- @doc conversion --- LDtk module. -- `ubiquitousse.ldtk` returns a single function, @{LDtk}. -- @section end --- Load a LDtk project. -- @string path to LDtk project file (.ldtk) -- @treturn Project the loaded LDtk project -- @function LDtk return (file) return project_mt._init(readJson(file), file:match("^(.-)[^%/%\\]+$")) end