mirror of
				https://github.com/Reuh/ubiquitousse.git
				synced 2025-10-27 17:19:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			625 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			625 lines
		
	
	
	
		
			19 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.
 | |
| --
 | |
| -- 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.
 | |
| -- 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
 | |
| 
 | |
| --- 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
 | |
| 
 | |
| --- json helpers
 | |
| let json_decode = require((...):gsub("ldtk$", "json")).decode
 | |
| 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}
 | |
| 
 | |
| --- returns a lua table from some fieldInstances
 | |
| let toLua = (type, val)
 | |
| 	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)
 | |
| 		end
 | |
| 	elseif type == "Color" then
 | |
| 		return parseColor(val)
 | |
| 	elseif type == "Point" then
 | |
| 		return { x = val.cx, y = val.cy }
 | |
| 	end
 | |
| 	return val
 | |
| end
 | |
| let getFields = (f)
 | |
| 	local t = {}
 | |
| 	for _, v in ipairs(f) do
 | |
| 		t[v.__identifier] = toLua(v.__type, v.__value)
 | |
| 	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
 | |
| let 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)
 | |
| 		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),
 | |
| 
 | |
| 			_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).
 | |
| 	-- @require love
 | |
| 	draw = :()
 | |
| 		if @visible then
 | |
| 			lg.push()
 | |
| 				lg.translate(@offsetX, @offsetY)
 | |
| 				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)
 | |
| 		let gridSize = layer.__gridSize
 | |
| 		let t = {
 | |
| 			--- @{Level} this layer belongs to.
 | |
| 			level = level,
 | |
| 			--- The layer name.
 | |
| 			identifier = layer.__identifier,
 | |
| 			--- Type of layer: IntGrid, Entities, Tiles or AutoLayer (string).
 | |
| 			type = layer.__type,
 | |
| 			--- Whether the layer is visible or not.
 | |
| 			visible = layer.visible,
 | |
| 			--- The layer opacity (0-1).
 | |
| 			opacity = layer.opacity,
 | |
| 			--- The layer order: smaller order means it is on top.
 | |
| 			order = order,
 | |
| 			--- X position of the layer relative to the level.
 | |
| 			offsetX = layer.__pxTotalOffsetX,
 | |
| 			--- Y position of the layer relative to the level.
 | |
| 			offsetY = layer.__pxTotalOffsetY,
 | |
| 			--- Size of the grid on this layer.
 | |
| 			gridSize = gridSize,
 | |
| 			--- Width of the layer, in grid units.
 | |
| 			gridWidth = layer.__cWid,
 | |
| 			--- Height of the layer, in grid units.
 | |
| 			gridHeight = layer.__cHei,
 | |
| 			--- _(Entities layer only)_ List of @{Entity} in the layer.
 | |
| 			entities = nil,
 | |
| 			--- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ List of @{Tile}s in the layer.
 | |
| 			tiles = nil,
 | |
| 			--- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ @{Tileset} object associated with the layer.
 | |
| 			tileset = nil,
 | |
| 			--- _(Tiles, AutoLayer, or IntGrid with AutoLayer rules layers only)_ [LÖVE SpriteBatch](https://love2d.org/wiki/SpriteBatch) containing the layer.
 | |
| 			-- nil if LÖVE not available.
 | |
| 			-- @require love
 | |
| 			spritebatch = nil,
 | |
| 			--- _(IntGrid without AutoLayer rules layer only)_ list of @{IntTile}s in the layer.
 | |
| 			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 @{tiles} list or `onAddTile` level load callback.
 | |
| 				--
 | |
| 				-- @type Tile
 | |
| 				let tile = {
 | |
| 					--- Layer the tile belongs to.
 | |
| 					layer = t,
 | |
| 					--- X position of the tile relative to the layer.
 | |
| 					x = x,
 | |
| 					--- Y position of the tile relative to the layer.
 | |
| 					y = y,
 | |
| 					--- Whether the tile is flipped horizontally.
 | |
| 					flipX = false,
 | |
| 					--- Whether the tile is flipped vertically.
 | |
| 					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.
 | |
| 					tags = tilesetData[tl.t].tags,
 | |
| 					--- Custom data associated with the tile, if any.
 | |
| 					data = tilesetData[tl.t].data,
 | |
| 					--- Quad associated with the tile (relative to the layer's tileset).
 | |
| 					-- LÖVE Quad if LÖVE is available, otherwise a table `{ x, y, width, height }`.
 | |
| 					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 = level.project._layerDef[layer.layerDefUid].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.
 | |
| 						layer = t,
 | |
| 						--- X position of the IntTile relative to the layer.
 | |
| 						x = x,
 | |
| 						--- Y position of the IntTile relative to the layer.
 | |
| 						y = y,
 | |
| 						--- Name of the IntTile.
 | |
| 						identifier = values[tl].identifier,
 | |
| 						--- Integer value of the IntTile.
 | |
| 						value = tl,
 | |
| 						--- Color of the IntTile.
 | |
| 						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.
 | |
| 					layer = t,
 | |
| 					--- The entity name.
 | |
| 					identifier = e.__identifier,
 | |
| 					--- X position of the entity relative to the layer.
 | |
| 					x = e.px[1],
 | |
| 					--- Y position of the entity relative to the layer.
 | |
| 					y = e.px[2],
 | |
| 					--- The entity width.
 | |
| 					width = e.width,
 | |
| 					--- The entity height.
 | |
| 					height = e.height,
 | |
| 					--- Scale factor on x axis relative to original entity size.
 | |
| 					sx = e.width / entityDef.width,
 | |
| 					--- Scale factor on y axis relative to original entity size.
 | |
| 					sy = e.height / entityDef.height,
 | |
| 					--- The entity pivot point x position relative to the entity.
 | |
| 					pivotX = e.__pivot[1] * e.width,
 | |
| 					--- The entity pivot point x position relative to the entity.
 | |
| 					pivotY = e.__pivot[2] * e.height,
 | |
| 					--- Entity color.
 | |
| 					color = entityDef.color,
 | |
| 					--- 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 }`.
 | |
| 					tile = nil,
 | |
| 					--- Map of custom fields of the entity.
 | |
| 					fields = getFields(e.fieldInstances),
 | |
| 					--- 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.
 | |
| 					-- @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
 | |
| 					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
 | |
| 					}
 | |
| 				end
 | |
| 				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.
 | |
| 	-- Assumes we are currently in world coordinates (i.e. world top-left is at 0,0).
 | |
| 	-- The level must be loaded.
 | |
| 	-- Will draw the eventual backgrounds and all the layers in the level.
 | |
| 	-- @require love
 | |
| 	draw = :()
 | |
| 		assert(@loaded == true, "level not loaded")
 | |
| 		lg.push()
 | |
| 			lg.translate(@x, @y)
 | |
| 			-- background color
 | |
| 			lg.setColor(@background.color)
 | |
| 			lg.rectangle("fill", 0, 0, @width, @height)
 | |
| 			-- background image
 | |
| 			lg.setColor(white)
 | |
| 			let bgImage = @background.image
 | |
| 			if bgImage then
 | |
| 				lg.draw(bgImage.image, bgImage.quad, bgImage.x, bgImage.y, 0, bgImage.sx, bgImage.sy)
 | |
| 			end
 | |
| 			-- layers
 | |
| 			for _, l in ipairs(@layers) do
 | |
| 				l:draw()
 | |
| 			end
 | |
| 		lg.pop()
 | |
| 	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)
 | |
| 			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.
 | |
| 			project = project,
 | |
| 			--- Whether this level is currently loaded or not (boolean).
 | |
| 			loaded = false,
 | |
| 			--- The level name (string).
 | |
| 			identifier = level.identifier,
 | |
| 			--- The level x position (number).
 | |
| 			x = level.worldX,
 | |
| 			--- The level y position (number).
 | |
| 			y = level.worldY,
 | |
| 			--- The level width (number).
 | |
| 			width = level.pxWid,
 | |
| 			--- The level height (number).
 | |
| 			height = level.pxHei,
 | |
| 			--- Map of custom fields of the level (table).
 | |
| 			fields = getFields(level.fieldInstances),
 | |
| 			--- List of @{Layer}s in the level (table).
 | |
| 			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
 | |
| 			-- @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 == "0.9.3", "map made for LDtk version %s":format(project.jsonVersion))
 | |
| 		let t = {
 | |
| 			--- List of @{Level}s in this project.
 | |
| 			levels = nil,
 | |
| 
 | |
| 			-- private
 | |
| 			_directory = directory,
 | |
| 			_layerDef = nil,
 | |
| 			_tilesetData = nil,
 | |
| 			_entityData = nil,
 | |
| 		}
 | |
| 		t.levels = [
 | |
| 			for _, lvl in ipairs(project.levels) do
 | |
| 				push level_mt._init(lvl, t)
 | |
| 			end
 | |
| 		]
 | |
| 		t._tilesetData = [
 | |
| 			for _, ts in ipairs(project.defs.tilesets) do
 | |
| 				@[ts.uid] = {
 | |
| 					path = directory..ts.relPath
 | |
| 				}
 | |
| 				local tilesetData = @[ts.uid]
 | |
| 				for gridx=0, ts.__cWid-1 do
 | |
| 					for gridy=0, ts.__cHei-1 do
 | |
| 						tilesetData[gridx + gridy * ts.__cWid] = {
 | |
| 							tags = {},
 | |
| 							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].tags, value)
 | |
| 						tilesetData[tileId].tags[value] = true
 | |
| 					end
 | |
| 				end
 | |
| 			end
 | |
| 		]
 | |
| 		t._layerDef = [
 | |
| 			for _, lay in ipairs(project.defs.layers) do
 | |
| 				@[lay.uid] = {
 | |
| 					intGridValues = nil
 | |
| 				}
 | |
| 				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] = {
 | |
| 					color = parseColor(ent.color),
 | |
| 					width = ent.width,
 | |
| 					height = ent.height
 | |
| 				}
 | |
| 			end
 | |
| 		]
 | |
| 		return setmetatable(t, project_mt)
 | |
| 	end
 | |
| }
 | |
| project_mt.__index = project_mt
 | |
| 
 | |
| --- Custom fields.
 | |
| -- TODO
 | |
| -- @section fields
 | |
| 
 | |
| --- 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
 |