mirror of
				https://github.com/Reuh/ubiquitousse.git
				synced 2025-10-27 17:19:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			824 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			824 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| --- [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 not loaded")
 | |
| 		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 not loaded")
 | |
| 		-- 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 already loaded")
 | |
| 		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 not loaded")
 | |
| 		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
 |