diff --git a/ldtk/ldtk.can b/ldtk/ldtk.can index 52caf16..047579d 100644 --- a/ldtk/ldtk.can +++ b/ldtk/ldtk.can @@ -1,13 +1,20 @@ --- [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). -- --- No mandatory dependency. --- Optionally requires LÖVE `love.graphics` (drawing Image, SpriteBatch, Quad) for drawing only. +-- 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 @@ -33,6 +40,8 @@ -- 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 @@ -45,8 +54,15 @@ else end end +let cache + --- json helpers -let json_decode = require((...):gsub("ldtk$", "json")).decode +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")) @@ -62,25 +78,61 @@ let parseColor = (str) 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) +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) + val[i] = toLua(itype, v, parent_entity) end elseif type == "Color" then return parseColor(val) elseif type == "Point" then - return { x = val.cx, y = val.cy } + 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) +let getFields = (f, parent_entity) local t = {} for _, v in ipairs(f) do - t[v.__identifier] = toLua(v.__type, v.__value) + t[v.__identifier] = toLua(v.__type, v.__value, parent_entity) end return t end @@ -99,7 +151,7 @@ let make_cache = (new_fn) end }) end -let cache = { +cache = { tileset = make_cache((tilesetDef) return tileset_mt._init(tilesetDef) end), @@ -126,11 +178,17 @@ tileset_mt = { 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) @@ -193,11 +251,15 @@ let layer_mt = { 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, @@ -228,7 +290,17 @@ let layer_mt = { --- 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. @@ -272,10 +344,10 @@ let layer_mt = { --- `Layer` the tile belongs to. -- @ftype Layer layer = t, - --- X position of the tile relative to the layer. + --- X position of the tile relative to the layer, in pixels. -- @ftype number x = x, - --- Y position of the tile relative to the layer. + --- Y position of the tile relative to the layer, in pixels. -- @ftype number y = y, --- Whether the tile is flipped horizontally. @@ -286,7 +358,7 @@ let layer_mt = { 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].tags, + tags = tilesetData[tl.t].enumTags, --- Custom data associated with the tile, if any. -- @ftype string data = tilesetData[tl.t].data, @@ -313,7 +385,7 @@ let layer_mt = { elseif layer.__type == "IntGrid" then t.intTiles = {} local onAddIntTile = callbacks.onAddIntTile - local values = level.project._layerDef[layer.layerDefUid].intGridValues + 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 @@ -329,10 +401,10 @@ let layer_mt = { --- `Layer` the IntTile belongs to. -- @ftype Layer layer = t, - --- X position of the IntTile relative to the layer. + --- X position of the IntTile relative to the layer, in pixels. -- @ftype number x = x, - --- Y position of the IntTile relative to the layer. + --- Y position of the IntTile relative to the layer, in pixels. -- @ftype number y = y, --- Name of the IntTile. @@ -367,19 +439,22 @@ let layer_mt = { --- `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. + --- 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. + --- Y position of the entity relative to the layer, in pixels. -- @ftype number y = e.px[2], - --- The entity width. + --- The entity width, in pixels. -- @ftype number width = e.width, - --- The entity height. + --- The entity height, in pixels. -- @ftype number height = e.height, --- Scale factor on x axis relative to original entity size. @@ -388,22 +463,25 @@ let layer_mt = { --- 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. + --- 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. + --- 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 = entityDef.color, + 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 = getFields(e.fieldInstances), + 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, @@ -421,14 +499,13 @@ let layer_mt = { end } if e.__tile then - local tileset = cache.tileset(level.project._tilesetData[e.__tile.tilesetUid]) - local srcRect = e.__tile.srcRect - local quad = tileset:_newQuad(srcRect[1], srcRect[2], srcRect[3], srcRect[4]) - entity.tile = { - tileset = tileset, - quad = quad - } + 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 @@ -528,6 +605,7 @@ let level_mt = { 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 @@ -564,13 +642,21 @@ let level_mt = { --- 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, - --- The level x position. + --- 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. + --- The level y position in pixels. + -- For Horizontal and Vertical layouts, is always -1. -- @ftype number y = level.worldY, --- The level width. @@ -583,6 +669,7 @@ let level_mt = { -- @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. @@ -612,9 +699,10 @@ level_mt.__index = level_mt -- @type Project let project_mt = { _init = (project, directory) - assert(project.jsonVersion == "0.9.3", "the map was made with LDtk version %s but the importer is made for 0.9.3":format(project.jsonVersion)) + 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, @@ -626,19 +714,29 @@ let project_mt = { } t.levels = [ for _, lvl in ipairs(project.levels) do - push level_mt._init(lvl, t) + 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] = { - path = directory..ts.relPath + 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] = { - tags = {}, + enumTags = {}, data = nil } end @@ -649,8 +747,8 @@ let project_mt = { for _, tag in ipairs(ts.enumTags) do local value = tag.enumValueId for _, tileId in ipairs(tag.tileIds) do - table.insert(tilesetData[tileId].tags, value) - tilesetData[tileId].tags[value] = true + table.insert(tilesetData[tileId].enumTags, value) + tilesetData[tileId].enumTags[value] = true end end end @@ -658,7 +756,10 @@ let project_mt = { t._layerDef = [ for _, lay in ipairs(project.defs.layers) do @[lay.uid] = { - intGridValues = nil + intGridValues = nil, + parallaxFactorX = lay.parallaxFactorX, + parallaxFactorY = lay.parallaxFactorY, + parallaxScaling = lay.parallaxScaling } local layerDef = @[lay.uid] if lay.__type == "IntGrid" then @@ -676,9 +777,9 @@ let project_mt = { t._entityData = [ for _, ent in ipairs(project.defs.entities) do @[ent.uid] = { - color = parseColor(ent.color), width = ent.width, - height = ent.height + height = ent.height, + nineSliceBorders = #ent.nineSliceBorders > 0 and ent.nineSliceBorders or nil } end ] @@ -704,8 +805,10 @@ project_mt.__index = project_mt -- * 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`: `{ x=number, y=number }`. +-- * 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.