1
0
Fork 0
mirror of https://github.com/Reuh/ubiquitousse.git synced 2025-10-27 17:19:31 +00:00

Update ldtk to LDtk 1.1.X

This commit is contained in:
Étienne Fildadut 2022-09-16 19:34:52 +09:00
parent bfbe236e58
commit 432ce84fbe

View file

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