소스 검색

Tiled map loading

Wolf Clement 1 년 전
부모
커밋
5a4e35b4ec
7개의 변경된 파일2500개의 추가작업 그리고 1개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 18 1
      main.lua
  3. 132 0
      sti/graphics.lua
  4. 1616 0
      sti/init.lua
  5. 323 0
      sti/plugins/box2d.lua
  6. 193 0
      sti/plugins/bump.lua
  7. 217 0
      sti/utils.lua

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+

+ 18 - 1
main.lua

@@ -6,9 +6,13 @@ local extension = jit.os == "Windows" and "dll" or jit.os == "Linux" and "so" or
 package.cpath = string.format("%s;%s/?.%s", package.cpath, lib_path, extension)
 local imgui = require "cimgui"
 
+local sti = require "sti"
+
 local lurker = require "lurker"
 lurker.postswap = function(file)
 	-- reinit game here
+
+    map = sti("assets/maps/world.lua")
 end
 
 love.load = function()
@@ -17,15 +21,26 @@ love.load = function()
 
 	local imio = imgui.GetIO()
 	imio.ConfigFlags = imgui.love.ConfigFlags("NavEnableKeyboard", "DockingEnable")
+
+    map = sti("assets/maps/world.lua")
 end
 
 love.draw = function()
 	-- draw game here
+    local game_canvas = love.graphics.newCanvas()
+    love.graphics.setCanvas(game_canvas)
+    map:draw(-60, -20, 8, 8)
+    love.graphics.setCanvas()
+    local size = imgui.ImVec2_Float(game_canvas:getDimensions())
 
 	local viewport = imgui.GetMainViewport()
 	imgui.DockSpaceOverViewport(viewport)
 
-    -- example window
+    -- draw windows here
+    if imgui.Begin("Game") then
+        imgui.Image(game_canvas, size)
+    end
+    imgui.End()
     imgui.ShowDemoWindow()
     
     -- code to render imgui
@@ -37,6 +52,8 @@ love.update = function(dt)
 	lurker.update()
     imgui.love.Update(dt)
     imgui.NewFrame()
+
+    map:update(dt)
 end
 
 love.mousemoved = function(x, y, ...)

+ 132 - 0
sti/graphics.lua

@@ -0,0 +1,132 @@
+local lg       = _G.love.graphics
+local graphics = { isCreated = lg and true or false }
+
+function graphics.newSpriteBatch(...)
+	if graphics.isCreated then
+		return lg.newSpriteBatch(...)
+	end
+end
+
+function graphics.newCanvas(...)
+	if graphics.isCreated then
+		return lg.newCanvas(...)
+	end
+end
+
+function graphics.newImage(...)
+	if graphics.isCreated then
+		return lg.newImage(...)
+	end
+end
+
+function graphics.newQuad(...)
+	if graphics.isCreated then
+		return lg.newQuad(...)
+	end
+end
+
+function graphics.getCanvas(...)
+	if graphics.isCreated then
+		return lg.getCanvas(...)
+	end
+end
+
+function graphics.setCanvas(...)
+	if graphics.isCreated then
+		return lg.setCanvas(...)
+	end
+end
+
+function graphics.clear(...)
+	if graphics.isCreated then
+		return lg.clear(...)
+	end
+end
+
+function graphics.push(...)
+	if graphics.isCreated then
+		return lg.push(...)
+	end
+end
+
+function graphics.origin(...)
+	if graphics.isCreated then
+		return lg.origin(...)
+	end
+end
+
+function graphics.scale(...)
+	if graphics.isCreated then
+		return lg.scale(...)
+	end
+end
+
+function graphics.translate(...)
+	if graphics.isCreated then
+		return lg.translate(...)
+	end
+end
+
+function graphics.pop(...)
+	if graphics.isCreated then
+		return lg.pop(...)
+	end
+end
+
+function graphics.draw(...)
+	if graphics.isCreated then
+		return lg.draw(...)
+	end
+end
+
+function graphics.rectangle(...)
+	if graphics.isCreated then
+		return lg.rectangle(...)
+	end
+end
+
+function graphics.getColor(...)
+	if graphics.isCreated then
+		return lg.getColor(...)
+	end
+end
+
+function graphics.setColor(...)
+	if graphics.isCreated then
+		return lg.setColor(...)
+	end
+end
+
+function graphics.line(...)
+	if graphics.isCreated then
+		return lg.line(...)
+	end
+end
+
+function graphics.polygon(...)
+	if graphics.isCreated then
+		return lg.polygon(...)
+	end
+end
+
+function graphics.points(...)
+	if graphics.isCreated then
+		return lg.points(...)
+	end
+end
+
+function graphics.getWidth()
+	if graphics.isCreated then
+		return lg.getWidth()
+	end
+	return 0
+end
+
+function graphics.getHeight()
+	if graphics.isCreated then
+		return lg.getHeight()
+	end
+	return 0
+end
+
+return graphics

+ 1616 - 0
sti/init.lua

@@ -0,0 +1,1616 @@
+--- Simple and fast Tiled map loader and renderer.
+-- @module sti
+-- @author Landon Manning
+-- @copyright 2019
+-- @license MIT/X11
+
+local STI = {
+	_LICENSE     = "MIT/X11",
+	_URL         = "https://github.com/karai17/Simple-Tiled-Implementation",
+	_VERSION     = "1.2.3.0",
+	_DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.",
+	cache        = {}
+}
+STI.__index = STI
+
+local love  = _G.love
+local cwd   = (...):gsub('%.init$', '') .. "."
+local utils = require(cwd .. "utils")
+local ceil  = math.ceil
+local floor = math.floor
+local lg    = require(cwd .. "graphics")
+local Map   = {}
+Map.__index = Map
+
+local function new(map, plugins, ox, oy)
+	local dir = ""
+
+	if type(map) == "table" then
+		map = setmetatable(map, Map)
+	else
+		-- Check for valid map type
+		local ext = map:sub(-4, -1)
+		assert(ext == ".lua", string.format(
+			"Invalid file type: %s. File must be of type: lua.",
+			ext
+		))
+
+		-- Get directory of map
+		dir = map:reverse():find("[/\\]") or ""
+		if dir ~= "" then
+			dir = map:sub(1, 1 + (#map - dir))
+		end
+
+		-- Load map
+		map = setmetatable(assert(love.filesystem.load(map))(), Map)
+	end
+
+	map:init(dir, plugins, ox, oy)
+
+	return map
+end
+
+--- Instance a new map.
+-- @param map Path to the map file or the map table itself
+-- @param plugins A list of plugins to load
+-- @param ox Offset of map on the X axis (in pixels)
+-- @param oy Offset of map on the Y axis (in pixels)
+-- @return table The loaded Map
+function STI.__call(_, map, plugins, ox, oy)
+	return new(map, plugins, ox, oy)
+end
+
+--- Flush image cache.
+function STI:flush()
+	self.cache = {}
+end
+
+--- Map object
+
+--- Instance a new map
+-- @param path Path to the map file
+-- @param plugins A list of plugins to load
+-- @param ox Offset of map on the X axis (in pixels)
+-- @param oy Offset of map on the Y axis (in pixels)
+function Map:init(path, plugins, ox, oy)
+	if type(plugins) == "table" then
+		self:loadPlugins(plugins)
+	end
+
+	self:resize()
+	self.objects       = {}
+	self.tiles         = {}
+	self.tileInstances = {}
+	self.drawRange     = {
+		sx = 1,
+		sy = 1,
+		ex = self.width,
+		ey = self.height,
+	}
+	self.offsetx = ox or 0
+	self.offsety = oy or 0
+
+	self.freeBatchSprites = {}
+	setmetatable(self.freeBatchSprites, { __mode = 'k' })
+
+	-- Set tiles, images
+	local gid = 1
+	for i, tileset in ipairs(self.tilesets) do
+		assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.")
+
+		-- Cache images
+		if lg.isCreated then
+			local formatted_path = utils.format_path(path .. tileset.image)
+
+			if not STI.cache[formatted_path] then
+				utils.fix_transparent_color(tileset, formatted_path)
+				utils.cache_image(STI, formatted_path, tileset.image)
+			else
+				tileset.image = STI.cache[formatted_path]
+			end
+		end
+
+		gid = self:setTiles(i, tileset, gid)
+	end
+
+	local layers = {}
+	for _, layer in ipairs(self.layers) do
+		self:groupAppendToList(layers, layer)
+	end
+	self.layers = layers
+
+	-- Set layers
+	for _, layer in ipairs(self.layers) do
+		self:setLayer(layer, path)
+	end
+end
+
+--- Layers from the group are added to the list
+-- @param layers List of layers
+-- @param layer Layer data
+function Map:groupAppendToList(layers, layer)
+	if layer.type == "group" then
+		for _, groupLayer in pairs(layer.layers) do
+			groupLayer.name = layer.name .. "." .. groupLayer.name
+			groupLayer.visible = layer.visible
+			groupLayer.opacity = layer.opacity * groupLayer.opacity
+			groupLayer.offsetx = layer.offsetx + groupLayer.offsetx
+			groupLayer.offsety = layer.offsety + groupLayer.offsety
+
+			for key, property in pairs(layer.properties) do
+				if groupLayer.properties[key] == nil then
+					groupLayer.properties[key] = property
+				end
+			end
+
+			self:groupAppendToList(layers, groupLayer)
+		end
+	else
+		table.insert(layers, layer)
+	end
+end
+
+--- Load plugins
+-- @param plugins A list of plugins to load
+function Map:loadPlugins(plugins)
+	for _, plugin in ipairs(plugins) do
+		local pluginModulePath = cwd .. 'plugins.' .. plugin
+		local ok, pluginModule = pcall(require, pluginModulePath)
+		if ok then
+			for k, func in pairs(pluginModule) do
+				if not self[k] then
+					self[k] = func
+				end
+			end
+		end
+	end
+end
+
+--- Create Tiles
+-- @param index Index of the Tileset
+-- @param tileset Tileset data
+-- @param gid First Global ID in Tileset
+-- @return number Next Tileset's first Global ID
+function Map:setTiles(index, tileset, gid)
+	local quad    = lg.newQuad
+	local imageW  = tileset.imagewidth
+	local imageH  = tileset.imageheight
+	local tileW   = tileset.tilewidth
+	local tileH   = tileset.tileheight
+	local margin  = tileset.margin
+	local spacing = tileset.spacing
+	local w       = utils.get_tiles(imageW, tileW, margin, spacing)
+	local h       = utils.get_tiles(imageH, tileH, margin, spacing)
+
+	for y = 1, h do
+		for x = 1, w do
+			local id    = gid - tileset.firstgid
+			local quadX = (x - 1) * tileW + margin + (x - 1) * spacing
+			local quadY = (y - 1) * tileH + margin + (y - 1) * spacing
+			local type = ""
+			local properties, terrain, animation, objectGroup
+
+			for _, tile in pairs(tileset.tiles) do
+				if tile.id == id then
+					properties  = tile.properties
+					animation   = tile.animation
+					objectGroup = tile.objectGroup
+					type        = tile.type
+
+					if tile.terrain then
+						terrain = {}
+
+						for i = 1, #tile.terrain do
+							terrain[i] = tileset.terrains[tile.terrain[i] + 1]
+						end
+					end
+				end
+			end
+
+			local tile = {
+				id          = id,
+				gid         = gid,
+				tileset     = index,
+				type        = type,
+				quad        = quad(
+					quadX,  quadY,
+					tileW,  tileH,
+					imageW, imageH
+				),
+				properties  = properties or {},
+				terrain     = terrain,
+				animation   = animation,
+				objectGroup = objectGroup,
+				frame       = 1,
+				time        = 0,
+				width       = tileW,
+				height      = tileH,
+				sx          = 1,
+				sy          = 1,
+				r           = 0,
+				offset      = tileset.tileoffset,
+			}
+
+			self.tiles[gid] = tile
+			gid             = gid + 1
+		end
+	end
+
+	return gid
+end
+
+--- Create Layers
+-- @param layer Layer data
+-- @param path (Optional) Path to an Image Layer's image
+function Map:setLayer(layer, path)
+	if layer.encoding then
+		if layer.encoding == "base64" then
+			assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".")
+			local fd = love.data.decode("string", "base64", layer.data)
+
+			if not layer.compression then
+				layer.data = utils.get_decompressed_data(fd)
+			else
+				assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".")
+
+				if layer.compression == "zlib" then
+					local data = love.data.decompress("string", "zlib", fd)
+					layer.data = utils.get_decompressed_data(data)
+				end
+
+				if layer.compression == "gzip" then
+					local data = love.data.decompress("string", "gzip", fd)
+					layer.data = utils.get_decompressed_data(data)
+				end
+			end
+		end
+	end
+
+	layer.x      = (layer.x or 0) + layer.offsetx + self.offsetx
+	layer.y      = (layer.y or 0) + layer.offsety + self.offsety
+	layer.update = function() end
+
+	if layer.type == "tilelayer" then
+		self:setTileData(layer)
+		self:setSpriteBatches(layer)
+		layer.draw = function() self:drawTileLayer(layer) end
+	elseif layer.type == "objectgroup" then
+		self:setObjectData(layer)
+		self:setObjectCoordinates(layer)
+		self:setObjectSpriteBatches(layer)
+		layer.draw = function() self:drawObjectLayer(layer) end
+	elseif layer.type == "imagelayer" then
+		layer.draw = function() self:drawImageLayer(layer) end
+
+		if layer.image ~= "" then
+			local formatted_path = utils.format_path(path .. layer.image)
+			if not STI.cache[formatted_path] then
+				utils.cache_image(STI, formatted_path)
+			end
+
+			layer.image  = STI.cache[formatted_path]
+			layer.width  = layer.image:getWidth()
+			layer.height = layer.image:getHeight()
+		end
+	end
+
+	self.layers[layer.name] = layer
+end
+
+--- Add Tiles to Tile Layer
+-- @param layer The Tile Layer
+function Map:setTileData(layer)
+	if layer.chunks then
+		for _, chunk in ipairs(layer.chunks) do
+			self:setTileData(chunk)
+		end
+		return
+	end
+
+	local i   = 1
+	local map = {}
+
+	for y = 1, layer.height do
+		map[y] = {}
+		for x = 1, layer.width do
+			local gid = layer.data[i]
+
+			-- NOTE: Empty tiles have a GID of 0
+			if gid > 0 then
+				map[y][x] = self.tiles[gid] or self:setFlippedGID(gid)
+			end
+
+			i = i + 1
+		end
+	end
+
+	layer.data = map
+end
+
+--- Add Objects to Layer
+-- @param layer The Object Layer
+function Map:setObjectData(layer)
+	for _, object in ipairs(layer.objects) do
+		object.layer            = layer
+		self.objects[object.id] = object
+	end
+end
+
+--- Correct position and orientation of Objects in an Object Layer
+-- @param layer The Object Layer
+function Map:setObjectCoordinates(layer)
+	for _, object in ipairs(layer.objects) do
+		local x   = layer.x + object.x
+		local y   = layer.y + object.y
+		local w   = object.width
+		local h   = object.height
+		local cos = math.cos(math.rad(object.rotation))
+		local sin = math.sin(math.rad(object.rotation))
+
+		if object.shape == "rectangle" and not object.gid then
+			object.rectangle = {}
+
+			local vertices = {
+				{ x=x,     y=y     },
+				{ x=x + w, y=y     },
+				{ x=x + w, y=y + h },
+				{ x=x,     y=y + h },
+			}
+
+			for _, vertex in ipairs(vertices) do
+				vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin)
+				table.insert(object.rectangle, { x = vertex.x, y = vertex.y })
+			end
+		elseif object.shape == "ellipse" then
+			object.ellipse = {}
+			local vertices = utils.convert_ellipse_to_polygon(x, y, w, h)
+
+			for _, vertex in ipairs(vertices) do
+				vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin)
+				table.insert(object.ellipse, { x = vertex.x, y = vertex.y })
+			end
+		elseif object.shape == "polygon" then
+			for _, vertex in ipairs(object.polygon) do
+				vertex.x           = vertex.x + x
+				vertex.y           = vertex.y + y
+				vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin)
+			end
+		elseif object.shape == "polyline" then
+			for _, vertex in ipairs(object.polyline) do
+				vertex.x           = vertex.x + x
+				vertex.y           = vertex.y + y
+				vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin)
+			end
+		end
+	end
+end
+
+--- Convert tile location to tile instance location
+-- @param layer Tile layer
+-- @param tile Tile
+-- @param x Tile location on X axis (in tiles)
+-- @param y Tile location on Y axis (in tiles)
+-- @return number Tile instance location on X axis (in pixels)
+-- @return number Tile instance location on Y axis (in pixels)
+function Map:getLayerTilePosition(layer, tile, x, y)
+	local tileW = self.tilewidth
+	local tileH = self.tileheight
+	local tileX, tileY
+
+	if self.orientation == "orthogonal" then
+		local tileset = self.tilesets[tile.tileset]
+		tileX = (x - 1) * tileW + tile.offset.x
+		tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight
+		tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH)
+	elseif self.orientation == "isometric" then
+		tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2
+		tileY = (x + y - 2) * (tileH / 2) + tile.offset.y
+	else
+		local sideLen = self.hexsidelength or 0
+		if self.staggeraxis == "y" then
+			if self.staggerindex == "odd" then
+				if y % 2 == 0 then
+					tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x
+				else
+					tileX = (x - 1) * tileW + tile.offset.x
+				end
+			else
+				if y % 2 == 0 then
+					tileX = (x - 1) * tileW + tile.offset.x
+				else
+					tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x
+				end
+			end
+
+			local rowH = tileH - (tileH - sideLen) / 2
+			tileY = (y - 1) * rowH + tile.offset.y
+		else
+			if self.staggerindex == "odd" then
+				if x % 2 == 0 then
+					tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y
+				else
+					tileY = (y - 1) * tileH + tile.offset.y
+				end
+			else
+				if x % 2 == 0 then
+					tileY = (y - 1) * tileH + tile.offset.y
+				else
+					tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y
+				end
+			end
+
+			local colW = tileW - (tileW - sideLen) / 2
+			tileX = (x - 1) * colW + tile.offset.x
+		end
+	end
+
+	return tileX, tileY
+end
+
+--- Place new tile instance
+-- @param layer Tile layer
+-- @param chunk Layer chunk
+-- @param tile Tile
+-- @param number Tile location on X axis (in tiles)
+-- @param number Tile location on Y axis (in tiles)
+function Map:addNewLayerTile(layer, chunk, tile, x, y)
+	local tileset = tile.tileset
+	local image   = self.tilesets[tile.tileset].image
+	local batches
+	local size
+
+	if chunk then
+		batches = chunk.batches
+		size    = chunk.width * chunk.height
+	else
+		batches = layer.batches
+		size    = layer.width * layer.height
+	end
+
+	batches[tileset] = batches[tileset] or lg.newSpriteBatch(image, size)
+
+	local batch = batches[tileset]
+	local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y)
+
+	local instance = {
+		layer = layer,
+		chunk = chunk,
+		gid   = tile.gid,
+		x     = tileX,
+		y     = tileY,
+		r     = tile.r,
+		oy    = 0
+	}
+
+	-- NOTE: STI can run headless so it is not guaranteed that a batch exists.
+	if batch then
+		instance.batch = batch
+		instance.id = batch:add(tile.quad, tileX, tileY, tile.r, tile.sx, tile.sy)
+	end
+
+	self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+	table.insert(self.tileInstances[tile.gid], instance)
+end
+
+function Map:set_batches(layer, chunk)
+	if chunk then
+		chunk.batches = {}
+	else
+		layer.batches = {}
+	end
+
+	if self.orientation == "orthogonal" or self.orientation == "isometric" then
+		local offsetX = chunk and chunk.x or 0
+		local offsetY = chunk and chunk.y or 0
+
+		local startX     = 1
+		local startY     = 1
+		local endX       = chunk and chunk.width  or layer.width
+		local endY       = chunk and chunk.height or layer.height
+		local incrementX = 1
+		local incrementY = 1
+
+		-- Determine order to add tiles to sprite batch
+		-- Defaults to right-down
+		if self.renderorder == "right-up" then
+			startY, endY, incrementY = endY, startY, -1
+		elseif self.renderorder == "left-down" then
+			startX, endX, incrementX = endX, startX, -1
+		elseif self.renderorder == "left-up" then
+			startX, endX, incrementX = endX, startX, -1
+			startY, endY, incrementY = endY, startY, -1
+		end
+
+		for y = startY, endY, incrementY do
+			for x = startX, endX, incrementX do
+				-- NOTE: Cannot short circuit this since it is valid for tile to be assigned nil
+				local tile
+				if chunk then
+					tile = chunk.data[y][x]
+				else
+					tile = layer.data[y][x]
+				end
+
+				if tile then
+					self:addNewLayerTile(layer, chunk, tile, x + offsetX, y + offsetY)
+				end
+			end
+		end
+	else
+		if self.staggeraxis == "y" then
+			for y = 1, (chunk and chunk.height or layer.height) do
+				for x = 1, (chunk and chunk.width or layer.width) do
+					-- NOTE: Cannot short circuit this since it is valid for tile to be assigned nil
+					local tile
+					if chunk then
+						tile = chunk.data[y][x]
+					else
+						tile = layer.data[y][x]
+					end
+
+					if tile then
+						self:addNewLayerTile(layer, chunk, tile, x, y)
+					end
+				end
+			end
+		else
+			local i = 0
+			local _x
+
+			if self.staggerindex == "odd" then
+				_x = 1
+			else
+				_x = 2
+			end
+
+			while i < (chunk and chunk.width * chunk.height or layer.width * layer.height) do
+				for _y = 1, (chunk and chunk.height or layer.height) + 0.5, 0.5 do
+					local y = floor(_y)
+
+					for x = _x, (chunk and chunk.width or layer.width), 2 do
+						i = i + 1
+
+						-- NOTE: Cannot short circuit this since it is valid for tile to be assigned nil
+						local tile
+						if chunk then
+							tile = chunk.data[y][x]
+						else
+							tile = layer.data[y][x]
+						end
+
+						if tile then
+							self:addNewLayerTile(layer, chunk, tile, x, y)
+						end
+					end
+
+					if _x == 1 then
+						_x = 2
+					else
+						_x = 1
+					end
+				end
+			end
+		end
+	end
+end
+
+--- Batch Tiles in Tile Layer for improved draw speed
+-- @param layer The Tile Layer
+function Map:setSpriteBatches(layer)
+	if layer.chunks then
+		for _, chunk in ipairs(layer.chunks) do
+			self:set_batches(layer, chunk)
+		end
+		return
+	end
+
+	self:set_batches(layer)
+end
+
+--- Batch Tiles in Object Layer for improved draw speed
+-- @param layer The Object Layer
+function Map:setObjectSpriteBatches(layer)
+	local newBatch = lg.newSpriteBatch
+	local batches  = {}
+
+	if layer.draworder == "topdown" then
+		table.sort(layer.objects, function(a, b)
+			return a.y + a.height < b.y + b.height
+		end)
+	end
+
+	for _, object in ipairs(layer.objects) do
+		if object.gid then
+			local tile    = self.tiles[object.gid] or self:setFlippedGID(object.gid)
+			local tileset = tile.tileset
+			local image   = self.tilesets[tileset].image
+
+			batches[tileset] = batches[tileset] or newBatch(image)
+
+			local sx = object.width  / tile.width
+			local sy = object.height / tile.height
+
+			-- Tiled rotates around bottom left corner, where love2D rotates around top left corner
+			local ox = 0
+			local oy = tile.height
+
+			local batch = batches[tileset]
+			local tileX = object.x + tile.offset.x
+			local tileY = object.y + tile.offset.y
+			local tileR = math.rad(object.rotation)
+
+			-- Compensation for scale/rotation shift
+			if tile.sx == -1 then
+				tileX = tileX + object.width
+
+				if tileR ~= 0 then
+					tileX = tileX - object.width
+					ox = ox + tile.width
+				end
+			end
+
+			if tile.sy == -1 then
+				tileY = tileY - object.height
+
+				if tileR ~= 0 then
+					tileY = tileY + object.width
+					oy = oy - tile.width
+				end
+			end
+
+			local instance = {
+				id    = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, ox, oy),
+				batch = batch,
+				layer = layer,
+				gid   = tile.gid,
+				x     = tileX,
+				y     = tileY - oy,
+				r     = tileR,
+				oy    = oy
+			}
+
+			self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+			table.insert(self.tileInstances[tile.gid], instance)
+		end
+	end
+
+	layer.batches = batches
+end
+
+--- Create a Custom Layer to place userdata in (such as player sprites)
+-- @param name Name of Custom Layer
+-- @param index Draw order within Layer stack
+-- @return table Custom Layer
+function Map:addCustomLayer(name, index)
+	index = index or #self.layers + 1
+	local layer = {
+      type       = "customlayer",
+      name       = name,
+      visible    = true,
+      opacity    = 1,
+      properties = {},
+    }
+
+	function layer.draw() end
+	function layer.update() end
+
+	table.insert(self.layers, index, layer)
+	self.layers[name] = self.layers[index]
+
+	return layer
+end
+
+--- Convert another Layer into a Custom Layer
+-- @param index Index or name of Layer to convert
+-- @return table Custom Layer
+function Map:convertToCustomLayer(index)
+	local layer = assert(self.layers[index], "Layer not found: " .. index)
+
+	layer.type     = "customlayer"
+	layer.x        = nil
+	layer.y        = nil
+	layer.width    = nil
+	layer.height   = nil
+	layer.encoding = nil
+	layer.data     = nil
+	layer.chunks   = nil
+	layer.objects  = nil
+	layer.image    = nil
+
+	function layer.draw() end
+	function layer.update() end
+
+	return layer
+end
+
+--- Remove a Layer from the Layer stack
+-- @param index Index or name of Layer to remove
+function Map:removeLayer(index)
+	local layer = assert(self.layers[index], "Layer not found: " .. index)
+
+	if type(index) == "string" then
+		for i, l in ipairs(self.layers) do
+			if l.name == index then
+				table.remove(self.layers, i)
+				self.layers[index] = nil
+				break
+			end
+		end
+	else
+		local name = self.layers[index].name
+		table.remove(self.layers, index)
+		self.layers[name] = nil
+	end
+
+	-- Remove layer batches
+	if layer.batches then
+		for _, batch in pairs(layer.batches) do
+			self.freeBatchSprites[batch] = nil
+		end
+	end
+
+	-- Remove chunk batches
+	if layer.chunks then
+		for _, chunk in ipairs(layer.chunks) do
+			for _, batch in pairs(chunk.batches) do
+				self.freeBatchSprites[batch] = nil
+			end
+		end
+	end
+
+	-- Remove tile instances
+	if layer.type == "tilelayer" then
+		for _, tiles in pairs(self.tileInstances) do
+			for i = #tiles, 1, -1 do
+				local tile = tiles[i]
+				if tile.layer == layer then
+					table.remove(tiles, i)
+				end
+			end
+		end
+	end
+
+	-- Remove objects
+	if layer.objects then
+		for i, object in pairs(self.objects) do
+			if object.layer == layer then
+				self.objects[i] = nil
+			end
+		end
+	end
+end
+
+--- Animate Tiles and update every Layer
+-- @param dt Delta Time
+function Map:update(dt)
+	for _, tile in pairs(self.tiles) do
+		local update = false
+
+		if tile.animation then
+			tile.time = tile.time + dt * 1000
+
+			while tile.time > tonumber(tile.animation[tile.frame].duration) do
+				update     = true
+				tile.time  = tile.time  - tonumber(tile.animation[tile.frame].duration)
+				tile.frame = tile.frame + 1
+
+				if tile.frame > #tile.animation then tile.frame = 1 end
+			end
+
+			if update and self.tileInstances[tile.gid] then
+				for _, j in pairs(self.tileInstances[tile.gid]) do
+					local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid]
+					j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy)
+				end
+			end
+		end
+	end
+
+	for _, layer in ipairs(self.layers) do
+		layer:update(dt)
+	end
+end
+
+--- Draw every Layer
+-- @param tx Translate on X
+-- @param ty Translate on Y
+-- @param sx Scale on X
+-- @param sy Scale on Y
+function Map:draw(tx, ty, sx, sy)
+	local current_canvas = lg.getCanvas()
+	lg.setCanvas(self.canvas)
+	lg.clear()
+
+	-- Scale map to 1.0 to draw onto canvas, this fixes tearing issues
+	-- Map is translated to correct position so the right section is drawn
+	lg.push()
+	lg.origin()
+	lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+	for _, layer in ipairs(self.layers) do
+		if layer.visible and layer.opacity > 0 then
+			self:drawLayer(layer)
+		end
+	end
+
+	lg.pop()
+
+	-- Draw canvas at 0,0; this fixes scissoring issues
+	-- Map is scaled to correct scale so the right section is shown
+	lg.push()
+	lg.origin()
+	lg.scale(sx or 1, sy or sx or 1)
+
+	lg.setCanvas(current_canvas)
+	lg.draw(self.canvas)
+
+	lg.pop()
+end
+
+--- Draw an individual Layer
+-- @param layer The Layer to draw
+function Map.drawLayer(_, layer)
+	local r,g,b,a = lg.getColor()
+	lg.setColor(r, g, b, a * layer.opacity)
+	layer:draw()
+	lg.setColor(r,g,b,a)
+end
+
+--- Default draw function for Tile Layers
+-- @param layer The Tile Layer to draw
+function Map:drawTileLayer(layer)
+	if type(layer) == "string" or type(layer) == "number" then
+		layer = self.layers[layer]
+	end
+
+	assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
+
+	-- NOTE: This does not take into account any sort of draw range clipping and will always draw every chunk
+	if layer.chunks then
+		for _, chunk in ipairs(layer.chunks) do
+			for _, batch in pairs(chunk.batches) do
+				lg.draw(batch, 0, 0)
+			end
+		end
+
+		return
+	end
+
+	for _, batch in pairs(layer.batches) do
+		lg.draw(batch, floor(layer.x), floor(layer.y))
+	end
+end
+
+--- Default draw function for Object Layers
+-- @param layer The Object Layer to draw
+function Map:drawObjectLayer(layer)
+	if type(layer) == "string" or type(layer) == "number" then
+		layer = self.layers[layer]
+	end
+
+	assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup")
+
+	local line  = { 160, 160, 160, 255 * layer.opacity       }
+	local fill  = { 160, 160, 160, 255 * layer.opacity * 0.5 }
+	local r,g,b,a = lg.getColor()
+	local reset = {   r,   g,   b,   a * layer.opacity       }
+
+	local function sortVertices(obj)
+		local vertex = {}
+
+		for _, v in ipairs(obj) do
+			table.insert(vertex, v.x)
+			table.insert(vertex, v.y)
+		end
+
+		return vertex
+	end
+
+	local function drawShape(obj, shape)
+		local vertex = sortVertices(obj)
+
+		if shape == "polyline" then
+			lg.setColor(line)
+			lg.line(vertex)
+			return
+		elseif shape == "polygon" then
+			lg.setColor(fill)
+			if not love.math.isConvex(vertex) then
+				local triangles = love.math.triangulate(vertex)
+				for _, triangle in ipairs(triangles) do
+					lg.polygon("fill", triangle)
+				end
+			else
+				lg.polygon("fill", vertex)
+			end
+		else
+			lg.setColor(fill)
+			lg.polygon("fill", vertex)
+		end
+
+		lg.setColor(line)
+		lg.polygon("line", vertex)
+	end
+
+	for _, object in ipairs(layer.objects) do
+		if object.visible then
+			if object.shape == "rectangle" and not object.gid then
+				drawShape(object.rectangle, "rectangle")
+			elseif object.shape == "ellipse" then
+				drawShape(object.ellipse, "ellipse")
+			elseif object.shape == "polygon" then
+				drawShape(object.polygon, "polygon")
+			elseif object.shape == "polyline" then
+				drawShape(object.polyline, "polyline")
+			elseif object.shape == "point" then
+				lg.points(object.x, object.y)
+			end
+		end
+	end
+
+	lg.setColor(reset)
+	for _, batch in pairs(layer.batches) do
+		lg.draw(batch, 0, 0)
+	end
+	lg.setColor(r,g,b,a)
+end
+
+--- Default draw function for Image Layers
+-- @param layer The Image Layer to draw
+function Map:drawImageLayer(layer)
+	if type(layer) == "string" or type(layer) == "number" then
+		layer = self.layers[layer]
+	end
+
+	assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer")
+
+	if layer.image ~= "" then
+		lg.draw(layer.image, layer.x, layer.y)
+	end
+end
+
+--- Resize the drawable area of the Map
+-- @param w The new width of the drawable area (in pixels)
+-- @param h The new Height of the drawable area (in pixels)
+function Map:resize(w, h)
+	if lg.isCreated then
+		w = w or lg.getWidth()
+		h = h or lg.getHeight()
+
+		self.canvas = lg.newCanvas(w, h)
+		self.canvas:setFilter("nearest", "nearest")
+	end
+end
+
+--- Create flipped or rotated Tiles based on bitop flags
+-- @param gid The flagged Global ID
+-- @return table Flipped Tile
+function Map:setFlippedGID(gid)
+	local bit31   = 2147483648
+	local bit30   = 1073741824
+	local bit29   = 536870912
+	local flipX   = false
+	local flipY   = false
+	local flipD   = false
+	local realgid = gid
+
+	if realgid >= bit31 then
+		realgid = realgid - bit31
+		flipX   = not flipX
+	end
+
+	if realgid >= bit30 then
+		realgid = realgid - bit30
+		flipY   = not flipY
+	end
+
+	if realgid >= bit29 then
+		realgid = realgid - bit29
+		flipD   = not flipD
+	end
+
+	local tile = self.tiles[realgid]
+	local data = {
+		id         = tile.id,
+		gid        = gid,
+		tileset    = tile.tileset,
+		frame      = tile.frame,
+		time       = tile.time,
+		width      = tile.width,
+		height     = tile.height,
+		offset     = tile.offset,
+		quad       = tile.quad,
+		properties = tile.properties,
+		terrain    = tile.terrain,
+		animation  = tile.animation,
+		sx         = tile.sx,
+		sy         = tile.sy,
+		r          = tile.r,
+	}
+
+	if flipX then
+		if flipY and flipD then
+			data.r  = math.rad(-90)
+			data.sy = -1
+		elseif flipY then
+			data.sx = -1
+			data.sy = -1
+		elseif flipD then
+			data.r = math.rad(90)
+		else
+			data.sx = -1
+		end
+	elseif flipY then
+		if flipD then
+			data.r = math.rad(-90)
+		else
+			data.sy = -1
+		end
+	elseif flipD then
+		data.r  = math.rad(90)
+		data.sy = -1
+	end
+
+	self.tiles[gid] = data
+
+	return self.tiles[gid]
+end
+
+--- Get custom properties from Layer
+-- @param layer The Layer
+-- @return table List of properties
+function Map:getLayerProperties(layer)
+	local l = self.layers[layer]
+
+	if not l then
+		return {}
+	end
+
+	return l.properties
+end
+
+--- Get custom properties from Tile
+-- @param layer The Layer that the Tile belongs to
+-- @param x The X axis location of the Tile (in tiles)
+-- @param y The Y axis location of the Tile (in tiles)
+-- @return table List of properties
+function Map:getTileProperties(layer, x, y)
+	local tile = self.layers[layer].data[y][x]
+
+	if not tile then
+		return {}
+	end
+
+	return tile.properties
+end
+
+--- Get custom properties from Object
+-- @param layer The Layer that the Object belongs to
+-- @param object The index or name of the Object
+-- @return table List of properties
+function Map:getObjectProperties(layer, object)
+	local o = self.layers[layer].objects
+
+	if type(object) == "number" then
+		o = o[object]
+	else
+		for _, v in ipairs(o) do
+			if v.name == object then
+				o = v
+				break
+			end
+		end
+	end
+
+	if not o then
+		return {}
+	end
+
+	return o.properties
+end
+
+--- Change a tile in a layer to another tile
+-- @param layer The Layer that the Tile belongs to
+-- @param x The X axis location of the Tile (in tiles)
+-- @param y The Y axis location of the Tile (in tiles)
+-- @param gid The gid of the new tile
+function Map:setLayerTile(layer, x, y, gid)
+	layer = self.layers[layer]
+
+	layer.data[y] = layer.data[y] or {}
+	local tile = layer.data[y][x]
+	local instance
+	if tile then
+		local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y)
+		for _, inst in pairs(self.tileInstances[tile.gid]) do
+			if inst.x == tileX and inst.y == tileY then
+				instance = inst
+				break
+			end
+		end
+	end
+
+	if tile == self.tiles[gid] then
+		return
+	end
+
+	tile = self.tiles[gid]
+
+	if instance then
+		self:swapTile(instance, tile)
+	else
+		self:addNewLayerTile(layer, tile, x, y)
+	end
+	layer.data[y][x] = tile
+end
+
+--- Swap a tile in a spritebatch
+-- @param instance The current Instance object we want to replace
+-- @param tile The Tile object we want to use
+-- @return none
+function Map:swapTile(instance, tile)
+	-- Update sprite batch
+	if instance.batch then
+		if tile then
+			instance.batch:set(
+				instance.id,
+				tile.quad,
+				instance.x,
+				instance.y,
+				tile.r,
+				tile.sx,
+				tile.sy
+			)
+		else
+			instance.batch:set(
+				instance.id,
+				instance.x,
+				instance.y,
+				0,
+				0)
+
+			self.freeBatchSprites[instance.batch] = self.freeBatchSprites[instance.batch] or {}
+			table.insert(self.freeBatchSprites[instance.batch], instance)
+		end
+	end
+
+	-- Remove old tile instance
+	for i, ins in ipairs(self.tileInstances[instance.gid]) do
+		if ins.batch == instance.batch and ins.id == instance.id then
+			table.remove(self.tileInstances[instance.gid], i)
+			break
+		end
+	end
+
+	-- Add new tile instance
+	if tile then
+		self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {}
+
+		local freeBatchSprites = self.freeBatchSprites[instance.batch]
+		local newInstance
+		if freeBatchSprites and #freeBatchSprites > 0 then
+			newInstance = freeBatchSprites[#freeBatchSprites]
+			freeBatchSprites[#freeBatchSprites] = nil
+		else
+			newInstance = {}
+		end
+
+		newInstance.layer = instance.layer
+		newInstance.batch = instance.batch
+		newInstance.id    = instance.id
+		newInstance.gid   = tile.gid or 0
+		newInstance.x     = instance.x
+		newInstance.y     = instance.y
+		newInstance.r     = tile.r or 0
+		newInstance.oy    = tile.r ~= 0 and tile.height or 0
+		table.insert(self.tileInstances[tile.gid], newInstance)
+	end
+end
+
+--- Convert tile location to pixel location
+-- @param x The X axis location of the point (in tiles)
+-- @param y The Y axis location of the point (in tiles)
+-- @return number The X axis location of the point (in pixels)
+-- @return number The Y axis location of the point (in pixels)
+function Map:convertTileToPixel(x,y)
+	if self.orientation == "orthogonal" then
+		local tileW = self.tilewidth
+		local tileH = self.tileheight
+		return
+			x * tileW,
+			y * tileH
+	elseif self.orientation == "isometric" then
+		local mapH    = self.height
+		local tileW   = self.tilewidth
+		local tileH   = self.tileheight
+		local offsetX = mapH * tileW / 2
+		return
+			(x - y) * tileW / 2 + offsetX,
+			(x + y) * tileH / 2
+	elseif self.orientation == "staggered" or
+		self.orientation     == "hexagonal" then
+		local tileW   = self.tilewidth
+		local tileH   = self.tileheight
+		local sideLen = self.hexsidelength or 0
+
+		if self.staggeraxis == "x" then
+			return
+				x * tileW,
+				ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0)
+		else
+			return
+				ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0),
+				y * tileH
+		end
+	end
+end
+
+--- Convert pixel location to tile location
+-- @param x The X axis location of the point (in pixels)
+-- @param y The Y axis location of the point (in pixels)
+-- @return number The X axis location of the point (in tiles)
+-- @return number The Y axis location of the point (in tiles)
+function Map:convertPixelToTile(x, y)
+	if self.orientation == "orthogonal" then
+		local tileW = self.tilewidth
+		local tileH = self.tileheight
+		return
+			x / tileW,
+			y / tileH
+	elseif self.orientation == "isometric" then
+		local mapH    = self.height
+		local tileW   = self.tilewidth
+		local tileH   = self.tileheight
+		local offsetX = mapH * tileW / 2
+		return
+			y / tileH + (x - offsetX) / tileW,
+			y / tileH - (x - offsetX) / tileW
+	elseif self.orientation == "staggered" then
+		local staggerX = self.staggeraxis  == "x"
+		local even     = self.staggerindex == "even"
+
+		local function topLeft(x, y)
+			if staggerX then
+				if ceil(x) % 2 == 1 and even then
+					return x - 1, y
+				else
+					return x - 1, y - 1
+				end
+			else
+				if ceil(y) % 2 == 1 and even then
+					return x, y - 1
+				else
+					return x - 1, y - 1
+				end
+			end
+		end
+
+		local function topRight(x, y)
+			if staggerX then
+				if ceil(x) % 2 == 1 and even then
+					return x + 1, y
+				else
+					return x + 1, y - 1
+				end
+			else
+				if ceil(y) % 2 == 1 and even then
+					return x + 1, y - 1
+				else
+					return x, y - 1
+				end
+			end
+		end
+
+		local function bottomLeft(x, y)
+			if staggerX then
+				if ceil(x) % 2 == 1 and even then
+					return x - 1, y + 1
+				else
+					return x - 1, y
+				end
+			else
+				if ceil(y) % 2 == 1 and even then
+					return x, y + 1
+				else
+					return x - 1, y + 1
+				end
+			end
+		end
+
+		local function bottomRight(x, y)
+			if staggerX then
+				if ceil(x) % 2 == 1 and even then
+					return x + 1, y + 1
+				else
+					return x + 1, y
+				end
+			else
+				if ceil(y) % 2 == 1 and even then
+					return x + 1, y + 1
+				else
+					return x, y + 1
+				end
+			end
+		end
+
+		local tileW = self.tilewidth
+		local tileH = self.tileheight
+
+		if staggerX then
+			x = x - (even and tileW / 2 or 0)
+		else
+			y = y - (even and tileH / 2 or 0)
+		end
+
+		local halfH      = tileH / 2
+		local ratio      = tileH / tileW
+		local referenceX = ceil(x / tileW)
+		local referenceY = ceil(y / tileH)
+		local relativeX  = x - referenceX * tileW
+		local relativeY  = y - referenceY * tileH
+
+		if (halfH - relativeX * ratio > relativeY) then
+			return topLeft(referenceX, referenceY)
+		elseif (-halfH + relativeX * ratio > relativeY) then
+			return topRight(referenceX, referenceY)
+		elseif (halfH + relativeX * ratio < relativeY) then
+			return bottomLeft(referenceX, referenceY)
+		elseif (halfH * 3 - relativeX * ratio < relativeY) then
+			return bottomRight(referenceX, referenceY)
+		end
+
+		return referenceX, referenceY
+	elseif self.orientation == "hexagonal" then
+		local staggerX  = self.staggeraxis  == "x"
+		local even      = self.staggerindex == "even"
+		local tileW     = self.tilewidth
+		local tileH     = self.tileheight
+		local sideLenX  = 0
+		local sideLenY  = 0
+
+		local colW       = tileW / 2
+		local rowH       = tileH / 2
+		if staggerX then
+			sideLenX = self.hexsidelength
+			x = x - (even and tileW or (tileW - sideLenX) / 2)
+			colW = colW - (colW  - sideLenX / 2) / 2
+		else
+			sideLenY = self.hexsidelength
+			y = y - (even and tileH or (tileH - sideLenY) / 2)
+			rowH = rowH - (rowH  - sideLenY / 2) / 2
+		end
+
+		local referenceX = ceil(x) / (colW * 2)
+		local referenceY = ceil(y) / (rowH * 2)
+
+    -- If in staggered line, then shift reference by 0.5 of other axes
+		if staggerX then
+			if (floor(referenceX) % 2 == 0) == even then
+				referenceY = referenceY - 0.5
+			end
+		else
+			if (floor(referenceY) % 2 == 0) == even then
+				referenceX = referenceX - 0.5
+			end
+		end
+
+		local relativeX  = x - referenceX * colW * 2
+		local relativeY  = y - referenceY * rowH * 2
+		local centers
+
+		if staggerX then
+			local left    = sideLenX / 2
+			local centerX = left + colW
+			local centerY = tileH / 2
+
+			centers = {
+				{ x = left,           y = centerY        },
+				{ x = centerX,        y = centerY - rowH },
+				{ x = centerX,        y = centerY + rowH },
+				{ x = centerX + colW, y = centerY        },
+			}
+		else
+			local top     = sideLenY / 2
+			local centerX = tileW / 2
+			local centerY = top + rowH
+
+			centers = {
+				{ x = centerX,        y = top },
+				{ x = centerX - colW, y = centerY },
+				{ x = centerX + colW, y = centerY },
+				{ x = centerX,        y = centerY + rowH }
+			}
+		end
+
+		local nearest = 0
+		local minDist = math.huge
+
+		local function len2(ax, ay)
+			return ax * ax + ay * ay
+		end
+
+		for i = 1, 4 do
+			local dc = len2(centers[i].x - relativeX, centers[i].y - relativeY)
+
+			if dc < minDist then
+				minDist = dc
+				nearest = i
+			end
+		end
+
+		local offsetsStaggerX = {
+			{ x = 1, y =  1 },
+			{ x = 2, y =  0 },
+			{ x = 2, y =  1 },
+			{ x = 3, y =  1 },
+		}
+
+		local offsetsStaggerY = {
+			{ x =  1, y = 1 },
+			{ x =  0, y = 2 },
+			{ x =  1, y = 2 },
+			{ x =  1, y = 3 },
+		}
+
+		local offsets = staggerX and offsetsStaggerX or offsetsStaggerY
+
+		return
+			referenceX + offsets[nearest].x,
+			referenceY + offsets[nearest].y
+	end
+end
+
+--- A list of individual layers indexed both by draw order and name
+-- @table Map.layers
+-- @see TileLayer
+-- @see ObjectLayer
+-- @see ImageLayer
+-- @see CustomLayer
+
+--- A list of individual tiles indexed by Global ID
+-- @table Map.tiles
+-- @see Tile
+-- @see Map.tileInstances
+
+--- A list of tile instances indexed by Global ID
+-- @table Map.tileInstances
+-- @see TileInstance
+-- @see Tile
+-- @see Map.tiles
+
+--- A list of no-longer-used batch sprites, indexed by batch
+--@table Map.freeBatchSprites
+
+--- A list of individual objects indexed by Global ID
+-- @table Map.objects
+-- @see Object
+
+--- @table TileLayer
+-- @field name The name of the layer
+-- @field x Position on the X axis (in pixels)
+-- @field y Position on the Y axis (in pixels)
+-- @field width Width of layer (in tiles)
+-- @field height Height of layer (in tiles)
+-- @field visible Toggle if layer is visible or hidden
+-- @field opacity Opacity of layer
+-- @field properties Custom properties
+-- @field data A tileWo dimensional table filled with individual tiles indexed by [y][x] (in tiles)
+-- @field update Update function
+-- @field draw Draw function
+-- @see Map.layers
+-- @see Tile
+
+--- @table ObjectLayer
+-- @field name The name of the layer
+-- @field x Position on the X axis (in pixels)
+-- @field y Position on the Y axis (in pixels)
+-- @field visible Toggle if layer is visible or hidden
+-- @field opacity Opacity of layer
+-- @field properties Custom properties
+-- @field objects List of objects indexed by draw order
+-- @field update Update function
+-- @field draw Draw function
+-- @see Map.layers
+-- @see Object
+
+--- @table ImageLayer
+-- @field name The name of the layer
+-- @field x Position on the X axis (in pixels)
+-- @field y Position on the Y axis (in pixels)
+-- @field visible Toggle if layer is visible or hidden
+-- @field opacity Opacity of layer
+-- @field properties Custom properties
+-- @field image Image to be drawn
+-- @field update Update function
+-- @field draw Draw function
+-- @see Map.layers
+
+--- Custom Layers are used to place userdata such as sprites within the draw order of the map.
+-- @table CustomLayer
+-- @field name The name of the layer
+-- @field x Position on the X axis (in pixels)
+-- @field y Position on the Y axis (in pixels)
+-- @field visible Toggle if layer is visible or hidden
+-- @field opacity Opacity of layer
+-- @field properties Custom properties
+-- @field update Update function
+-- @field draw Draw function
+-- @see Map.layers
+-- @usage
+--	-- Create a Custom Layer
+--	local spriteLayer = map:addCustomLayer("Sprite Layer", 3)
+--
+--	-- Add data to Custom Layer
+--	spriteLayer.sprites = {
+--		player = {
+--			image = lg.newImage("assets/sprites/player.png"),
+--			x = 64,
+--			y = 64,
+--			r = 0,
+--		}
+--	}
+--
+--	-- Update callback for Custom Layer
+--	function spriteLayer:update(dt)
+--		for _, sprite in pairs(self.sprites) do
+--			sprite.r = sprite.r + math.rad(90 * dt)
+--		end
+--	end
+--
+--	-- Draw callback for Custom Layer
+--	function spriteLayer:draw()
+--		for _, sprite in pairs(self.sprites) do
+--			local x = math.floor(sprite.x)
+--			local y = math.floor(sprite.y)
+--			local r = sprite.r
+--			lg.draw(sprite.image, x, y, r)
+--		end
+--	end
+
+--- @table Tile
+-- @field id Local ID within Tileset
+-- @field gid Global ID
+-- @field tileset Tileset ID
+-- @field quad Quad object
+-- @field properties Custom properties
+-- @field terrain Terrain data
+-- @field animation Animation data
+-- @field frame Current animation frame
+-- @field time Time spent on current animation frame
+-- @field width Width of tile
+-- @field height Height of tile
+-- @field sx Scale value on the X axis
+-- @field sy Scale value on the Y axis
+-- @field r Rotation of tile (in radians)
+-- @field offset Offset drawing position
+-- @field offset.x Offset value on the X axis
+-- @field offset.y Offset value on the Y axis
+-- @see Map.tiles
+
+--- @table TileInstance
+-- @field batch Spritebatch the Tile Instance belongs to
+-- @field id ID within the spritebatch
+-- @field gid Global ID
+-- @field x Position on the X axis (in pixels)
+-- @field y Position on the Y axis (in pixels)
+-- @see Map.tileInstances
+-- @see Tile
+
+--- @table Object
+-- @field id Global ID
+-- @field name Name of object (non-unique)
+-- @field shape Shape of object
+-- @field x Position of object on X axis (in pixels)
+-- @field y Position of object on Y axis (in pixels)
+-- @field width Width of object (in pixels)
+-- @field height Heigh tof object (in pixels)
+-- @field rotation Rotation of object (in radians)
+-- @field visible Toggle if object is visible or hidden
+-- @field properties Custom properties
+-- @field ellipse List of verticies of specific shape
+-- @field rectangle List of verticies of specific shape
+-- @field polygon List of verticies of specific shape
+-- @field polyline List of verticies of specific shape
+-- @see Map.objects
+
+return setmetatable({}, STI)

+ 323 - 0
sti/plugins/box2d.lua

@@ -0,0 +1,323 @@
+--- Box2D plugin for STI
+-- @module box2d
+-- @author Landon Manning
+-- @copyright 2019
+-- @license MIT/X11
+
+local love  = _G.love
+local utils = require((...):gsub('plugins.box2d', 'utils'))
+local lg    = require((...):gsub('plugins.box2d', 'graphics'))
+
+return {
+	box2d_LICENSE     = "MIT/X11",
+	box2d_URL         = "https://github.com/karai17/Simple-Tiled-Implementation",
+	box2d_VERSION     = "2.3.2.7",
+	box2d_DESCRIPTION = "Box2D hooks for STI.",
+
+	--- Initialize Box2D physics world.
+	-- @param world The Box2D world to add objects to.
+	box2d_init = function(map, world)
+		assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
+
+		local body      = love.physics.newBody(world, map.offsetx, map.offsety)
+		local collision = {
+			body = body,
+		}
+
+		local function addObjectToWorld(objshape, vertices, userdata, object)
+			local shape
+
+			if objshape == "polyline" then
+				if #vertices == 4 then
+					shape = love.physics.newEdgeShape(unpack(vertices))
+				else
+					shape = love.physics.newChainShape(false, unpack(vertices))
+				end
+			else
+				shape = love.physics.newPolygonShape(unpack(vertices))
+			end
+
+			local currentBody = body
+			--dynamic are objects/players etc.
+			if userdata.properties.dynamic == true then
+				currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
+			-- static means it shouldn't move. Things like walls/ground.
+			elseif userdata.properties.static == true then
+				currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'static')
+			-- kinematic means that the object is static in the game world but effects other bodies
+			elseif userdata.properties.kinematic == true then
+				currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'kinematic')			
+			end
+
+			local fixture = love.physics.newFixture(currentBody, shape)
+			fixture:setUserData(userdata)
+
+			-- Set some custom properties from userdata (or use default set by box2d)
+			fixture:setFriction(userdata.properties.friction       or 0.2)
+			fixture:setRestitution(userdata.properties.restitution or 0.0)
+			fixture:setSensor(userdata.properties.sensor           or false)
+			fixture:setFilterData(
+				userdata.properties.categories or 1,
+				userdata.properties.mask       or 65535,
+				userdata.properties.group      or 0
+			)
+
+			local obj = {
+				object  = object,
+				body    = currentBody,
+				shape   = shape,
+				fixture = fixture,
+			}
+
+			table.insert(collision, obj)
+		end
+
+		local function getPolygonVertices(object)
+			local vertices = {}
+			for _, vertex in ipairs(object.polygon) do
+				table.insert(vertices, vertex.x)
+				table.insert(vertices, vertex.y)
+			end
+
+			return vertices
+		end
+
+		local function calculateObjectPosition(object, tile)
+			local o = {
+				shape   = object.shape,
+				x       = (object.dx or object.x) + map.offsetx,
+				y       = (object.dy or object.y) + map.offsety,
+				w       = object.width,
+				h       = object.height,
+				polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
+			}
+
+			local userdata = {
+				object     = o,
+				properties = object.properties
+			}
+
+			o.r = object.rotation or 0
+			if o.shape == "rectangle" then
+				local cos = math.cos(math.rad(o.r))
+				local sin = math.sin(math.rad(o.r))
+				local oy  = 0
+
+				if object.gid then
+					local tileset = map.tilesets[map.tiles[object.gid].tileset]
+					local lid     = object.gid - tileset.firstgid
+					local t       = {}
+
+					-- This fixes a height issue
+					 o.y = o.y + map.tiles[object.gid].offset.y
+					 oy  = o.h
+
+					for _, tt in ipairs(tileset.tiles) do
+						if tt.id == lid then
+							t = tt
+							break
+						end
+					end
+
+					if t.objectGroup then
+						for _, obj in ipairs(t.objectGroup.objects) do
+							-- Every object in the tile
+							calculateObjectPosition(obj, object)
+						end
+
+						return
+					else
+						o.w = map.tiles[object.gid].width
+						o.h = map.tiles[object.gid].height
+					end
+				end
+
+				o.polygon = {
+					{ x=o.x+0,   y=o.y+0   },
+					{ x=o.x+o.w, y=o.y+0   },
+					{ x=o.x+o.w, y=o.y+o.h },
+					{ x=o.x+0,   y=o.y+o.h }
+				}
+
+				for _, vertex in ipairs(o.polygon) do
+					vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy)
+				end
+
+				local vertices = getPolygonVertices(o)
+				addObjectToWorld(o.shape, vertices, userdata, tile or object)
+			elseif o.shape == "ellipse" then
+				if not o.polygon then
+					o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h)
+				end
+				local vertices  = getPolygonVertices(o)
+				local triangles = love.math.triangulate(vertices)
+
+				for _, triangle in ipairs(triangles) do
+					addObjectToWorld(o.shape, triangle, userdata, tile or object)
+				end
+			elseif o.shape == "polygon" then
+				-- Recalculate collision polygons inside tiles
+				if tile then
+					local cos = math.cos(math.rad(o.r))
+					local sin = math.sin(math.rad(o.r))
+					for _, vertex in ipairs(o.polygon) do
+						vertex.x = vertex.x + o.x
+						vertex.y = vertex.y + o.y
+						vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin)
+					end
+				end
+
+				local vertices  = getPolygonVertices(o)
+				local triangles = love.math.triangulate(vertices)
+
+				for _, triangle in ipairs(triangles) do
+					addObjectToWorld(o.shape, triangle, userdata, tile or object)
+				end
+			elseif o.shape == "polyline" then
+				local vertices = getPolygonVertices(o)
+				addObjectToWorld(o.shape, vertices, userdata, tile or object)
+			end
+		end
+
+		for _, tile in pairs(map.tiles) do
+			if map.tileInstances[tile.gid] then
+				for _, instance in ipairs(map.tileInstances[tile.gid]) do
+					-- Every object in every instance of a tile
+					if tile.objectGroup then
+						for _, object in ipairs(tile.objectGroup.objects) do
+							if object.properties.collidable == true then
+								object = utils.deepCopy(object)
+								object.dx = instance.x + object.x
+								object.dy = instance.y + object.y
+								calculateObjectPosition(object, instance)
+							end
+						end
+					end
+
+					-- Every instance of a tile
+					if tile.properties.collidable == true then
+						local object = {
+							shape      = "rectangle",
+							x          = instance.x,
+							y          = instance.y,
+							width      = map.tilewidth,
+							height     = map.tileheight,
+							properties = tile.properties
+						}
+
+						calculateObjectPosition(object, instance)
+					end
+				end
+			end
+		end
+
+		for _, layer in ipairs(map.layers) do
+			-- Entire layer
+			if layer.properties.collidable == true then
+				if layer.type == "tilelayer" then
+					for gid, tiles in pairs(map.tileInstances) do
+						local tile = map.tiles[gid]
+						local tileset = map.tilesets[tile.tileset]
+
+						for _, instance in ipairs(tiles) do
+							if instance.layer == layer then
+								local object = {
+									shape      = "rectangle",
+									x          = instance.x,
+									y          = instance.y,
+									width      = tileset.tilewidth,
+									height     = tileset.tileheight,
+									properties = tile.properties
+								}
+
+								calculateObjectPosition(object, instance)
+							end
+						end
+					end
+				elseif layer.type == "objectgroup" then
+					for _, object in ipairs(layer.objects) do
+						calculateObjectPosition(object)
+					end
+				elseif layer.type == "imagelayer" then
+					local object = {
+						shape      = "rectangle",
+						x          = layer.x or 0,
+						y          = layer.y or 0,
+						width      = layer.width,
+						height     = layer.height,
+						properties = layer.properties
+					}
+
+					calculateObjectPosition(object)
+				end
+			end
+
+			-- Individual objects
+			if layer.type == "objectgroup" then
+				for _, object in ipairs(layer.objects) do
+					if object.properties.collidable == true then
+						calculateObjectPosition(object)
+					end
+				end
+			end
+		end
+
+		map.box2d_collision = collision
+	end,
+
+	--- Remove Box2D fixtures and shapes from world.
+	-- @param index The index or name of the layer being removed
+	box2d_removeLayer = function(map, index)
+		local layer = assert(map.layers[index], "Layer not found: " .. index)
+		local collision = map.box2d_collision
+
+		-- Remove collision objects
+		for i = #collision, 1, -1 do
+			local obj = collision[i]
+
+			if obj.object.layer == layer then
+				obj.fixture:destroy()
+				table.remove(collision, i)
+			end
+		end
+	end,
+
+	--- Draw Box2D physics world.
+	-- @param tx Translate on X
+	-- @param ty Translate on Y
+	-- @param sx Scale on X
+	-- @param sy Scale on Y
+	box2d_draw = function(map, tx, ty, sx, sy)
+		local collision = map.box2d_collision
+
+		lg.push()
+		lg.scale(sx or 1, sy or sx or 1)
+		lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+		for _, obj in ipairs(collision) do
+			local points = {obj.body:getWorldPoints(obj.shape:getPoints())}
+			local shape_type = obj.shape:getType()
+
+			if shape_type == "edge" or shape_type == "chain" then
+				love.graphics.line(points)
+			elseif shape_type == "polygon" then
+				love.graphics.polygon("line", points)
+			else
+				error("sti box2d plugin does not support "..shape_type.." shapes")
+			end
+		end
+
+		lg.pop()
+	end
+}
+
+--- Custom Properties in Tiled are used to tell this plugin what to do.
+-- @table Properties
+-- @field collidable set to true, can be used on any Layer, Tile, or Object
+-- @field sensor set to true, can be used on any Tile or Object that is also collidable
+-- @field dynamic set to true, can be used on any Tile or Object
+-- @field friction can be used to define the friction of any Object
+-- @field restitution can be used to define the restitution of any Object
+-- @field categories can be used to set the filter Category of any Object
+-- @field mask can be used to set the filter Mask of any Object
+-- @field group can be used to set the filter Group of any Object

+ 193 - 0
sti/plugins/bump.lua

@@ -0,0 +1,193 @@
+--- Bump.lua plugin for STI
+-- @module bump.lua
+-- @author David Serrano (BobbyJones|FrenchFryLord)
+-- @copyright 2019
+-- @license MIT/X11
+
+local lg = require((...):gsub('plugins.bump', 'graphics'))
+
+return {
+	bump_LICENSE        = "MIT/X11",
+	bump_URL            = "https://github.com/karai17/Simple-Tiled-Implementation",
+	bump_VERSION        = "3.1.7.1",
+	bump_DESCRIPTION    = "Bump hooks for STI.",
+
+	--- Adds each collidable tile to the Bump world.
+	-- @param world The Bump world to add objects to.
+	-- @return collidables table containing the handles to the objects in the Bump world.
+	bump_init = function(map, world)
+		local collidables = {}
+
+		for _, tileset in ipairs(map.tilesets) do
+			for _, tile in ipairs(tileset.tiles) do
+				local gid = tileset.firstgid + tile.id
+
+				if map.tileInstances[gid] then
+					for _, instance in ipairs(map.tileInstances[gid]) do
+						-- Every object in every instance of a tile
+						if tile.objectGroup then
+							for _, object in ipairs(tile.objectGroup.objects) do
+								if object.properties.collidable == true then
+									local t = {
+										name       = object.name,
+										type       = object.type,
+										x          = instance.x + map.offsetx + object.x,
+										y          = instance.y + map.offsety + object.y,
+										width      = object.width,
+										height     = object.height,
+										layer      = instance.layer,
+										properties = object.properties
+
+									}
+
+									world:add(t, t.x, t.y, t.width, t.height)
+									table.insert(collidables, t)
+								end
+							end
+						end
+
+						-- Every instance of a tile
+						if tile.properties and tile.properties.collidable == true then
+							local t = {
+								x          = instance.x + map.offsetx,
+								y          = instance.y + map.offsety,
+								width      = map.tilewidth,
+								height     = map.tileheight,
+								layer      = instance.layer,
+								type       = tile.type,
+								properties = tile.properties
+							}
+
+							world:add(t, t.x, t.y, t.width, t.height)
+							table.insert(collidables, t)
+						end
+					end
+				end
+			end
+		end
+
+		for _, layer in ipairs(map.layers) do
+			-- Entire layer
+			if layer.properties.collidable == true then
+				if layer.type == "tilelayer" then
+					for y, tiles in ipairs(layer.data) do
+						for x, tile in pairs(tiles) do
+
+							if tile.objectGroup then
+								for _, object in ipairs(tile.objectGroup.objects) do
+									if object.properties.collidable == true then
+										local t = {
+											name       = object.name,
+											type       = object.type,
+											x          = ((x-1) * map.tilewidth  + tile.offset.x + map.offsetx) + object.x,
+											y          = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y,
+											width      = object.width,
+											height     = object.height,
+											layer      = layer,
+											properties = object.properties
+										}
+
+										world:add(t, t.x, t.y, t.width, t.height)
+										table.insert(collidables, t)
+									end
+								end
+							end
+
+
+							local t = {
+								x          = (x-1) * map.tilewidth  + tile.offset.x + map.offsetx,
+								y          = (y-1) * map.tileheight + tile.offset.y + map.offsety,
+								width      = tile.width,
+								height     = tile.height,
+								layer      = layer,
+								type       = tile.type,
+								properties = tile.properties
+							}
+
+							world:add(t, t.x, t.y, t.width, t.height)
+							table.insert(collidables, t)
+						end
+					end
+				elseif layer.type == "imagelayer" then
+					world:add(layer, layer.x, layer.y, layer.width, layer.height)
+					table.insert(collidables, layer)
+				end
+		  end
+
+			-- individual collidable objects in a layer that is not "collidable"
+			-- or whole collidable objects layer
+		  if layer.type == "objectgroup" then
+				for _, obj in ipairs(layer.objects) do
+					if layer.properties.collidable == true or obj.properties.collidable == true then
+						if obj.shape == "rectangle" then
+							local t = {
+								name       = obj.name,
+								type       = obj.type,
+								x          = obj.x + map.offsetx,
+								y          = obj.y + map.offsety,
+								width      = obj.width,
+								height     = obj.height,
+								layer      = layer,
+								properties = obj.properties
+							}
+
+							if obj.gid then
+								t.y = t.y - obj.height
+							end
+
+							world:add(t, t.x, t.y, t.width, t.height)
+							table.insert(collidables, t)
+						end -- TODO implement other object shapes?
+					end
+				end
+			end
+		end
+
+		map.bump_world       = world
+		map.bump_collidables = collidables
+	end,
+
+	--- Remove layer
+	-- @param index to layer to be removed
+	bump_removeLayer = function(map, index)
+		local layer = assert(map.layers[index], "Layer not found: " .. index)
+		local collidables = map.bump_collidables
+
+		-- Remove collision objects
+		for i = #collidables, 1, -1 do
+			local obj = collidables[i]
+
+			if obj.layer == layer
+			and (
+				layer.properties.collidable == true
+				or obj.properties.collidable == true
+			) then
+				map.bump_world:remove(obj)
+				table.remove(collidables, i)
+			end
+		end
+	end,
+
+	--- Draw bump collisions world.
+	-- @param world bump world holding the tiles geometry
+	-- @param tx Translate on X
+	-- @param ty Translate on Y
+	-- @param sx Scale on X
+	-- @param sy Scale on Y
+	bump_draw = function(map, tx, ty, sx, sy)
+		lg.push()
+		lg.scale(sx or 1, sy or sx or 1)
+		lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+
+		local items = map.bump_world:getItems()
+		for _, item in ipairs(items) do
+			lg.rectangle("line", map.bump_world:getRect(item))
+		end
+
+		lg.pop()
+	end
+}
+
+--- Custom Properties in Tiled are used to tell this plugin what to do.
+-- @table Properties
+-- @field collidable set to true, can be used on any Layer, Tile, or Object

+ 217 - 0
sti/utils.lua

@@ -0,0 +1,217 @@
+-- Some utility functions that shouldn't be exposed.
+local utils = {}
+
+-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286
+function utils.format_path(path)
+	local np_gen1,np_gen2  = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
+	local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/')
+	local k
+
+	repeat -- /./ -> /
+		path,k = path:gsub(np_pat2,'/',1)
+	until k == 0
+
+	repeat -- A/../ -> (empty)
+		path,k = path:gsub(np_pat1,'',1)
+	until k == 0
+
+	if path == '' then path = '.' end
+
+	return path
+end
+
+-- Compensation for scale/rotation shift
+function utils.compensate(tile, tileX, tileY, tileW, tileH)
+	local compx = 0
+	local compy = 0
+
+	if tile.sx < 0 then compx = tileW end
+	if tile.sy < 0 then compy = tileH end
+
+	if tile.r > 0 then
+		tileX = tileX + tileH - compy
+		tileY = tileY + tileH + compx - tileW
+	elseif tile.r < 0 then
+		tileX = tileX + compy
+		tileY = tileY - compx + tileH
+	else
+		tileX = tileX + compx
+		tileY = tileY + compy
+	end
+
+	return tileX, tileY
+end
+
+-- Cache images in main STI module
+function utils.cache_image(sti, path, image)
+	image = image or love.graphics.newImage(path)
+	image:setFilter("nearest", "nearest")
+	sti.cache[path] = image
+end
+
+-- We just don't know.
+function utils.get_tiles(imageW, tileW, margin, spacing)
+	imageW  = imageW - margin
+	local n = 0
+
+	while imageW >= tileW do
+		imageW = imageW - tileW
+		if n ~= 0 then imageW = imageW - spacing end
+		if imageW >= 0 then n  = n + 1 end
+	end
+
+	return n
+end
+
+-- Decompress tile layer data
+function utils.get_decompressed_data(data)
+	local ffi     = require "ffi"
+	local d       = {}
+	local decoded = ffi.cast("uint32_t*", data)
+
+	for i = 0, data:len() / ffi.sizeof("uint32_t") do
+		table.insert(d, tonumber(decoded[i]))
+	end
+
+	return d
+end
+
+-- Convert a Tiled ellipse object to a LOVE polygon
+function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments)
+	local ceil = math.ceil
+	local cos  = math.cos
+	local sin  = math.sin
+
+	local function calc_segments(segments)
+		local function vdist(a, b)
+			local c = {
+				x = a.x - b.x,
+				y = a.y - b.y,
+			}
+
+			return c.x * c.x + c.y * c.y
+		end
+
+		segments = segments or 64
+		local vertices = {}
+
+		local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) }
+
+		local m
+		if love and love.physics then
+			m = love.physics.getMeter()
+		else
+			m = 32
+		end
+
+		for _, i in ipairs(v) do
+			local angle = (i / segments) * math.pi * 2
+			local px    = x + w / 2 + cos(angle) * w / 2
+			local py    = y + h / 2 + sin(angle) * h / 2
+
+			table.insert(vertices, { x = px / m, y = py / m })
+		end
+
+		local dist1 = vdist(vertices[1], vertices[2])
+		local dist2 = vdist(vertices[3], vertices[4])
+
+		-- Box2D threshold
+		if dist1 < 0.0025 or dist2 < 0.0025 then
+			return calc_segments(segments-2)
+		end
+
+		return segments
+	end
+
+	local segments = calc_segments(max_segments)
+	local vertices = {}
+
+	table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
+
+	for i = 0, segments do
+		local angle = (i / segments) * math.pi * 2
+		local px    = x + w / 2 + cos(angle) * w / 2
+		local py    = y + h / 2 + sin(angle) * h / 2
+
+		table.insert(vertices, { x = px, y = py })
+	end
+
+	return vertices
+end
+
+function utils.rotate_vertex(map, vertex, x, y, cos, sin, oy)
+	if map.orientation == "isometric" then
+		x, y               = utils.convert_isometric_to_screen(map, x, y)
+		vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y)
+	end
+
+	vertex.x = vertex.x - x
+	vertex.y = vertex.y - y
+
+	return
+		x + cos * vertex.x - sin * vertex.y,
+		y + sin * vertex.x + cos * vertex.y - (oy or 0)
+end
+
+--- Project isometric position to cartesian position
+function utils.convert_isometric_to_screen(map, x, y)
+	local mapW    = map.width
+	local tileW   = map.tilewidth
+	local tileH   = map.tileheight
+	local tileX   = x / tileH
+	local tileY   = y / tileH
+	local offsetX = mapW * tileW / 2
+
+	return
+		(tileX - tileY) * tileW / 2 + offsetX,
+		(tileX + tileY) * tileH / 2
+end
+
+function utils.hex_to_color(hex)
+	if hex:sub(1, 1) == "#" then
+		hex = hex:sub(2)
+	end
+
+	return {
+		r = tonumber(hex:sub(1, 2), 16) / 255,
+		g = tonumber(hex:sub(3, 4), 16) / 255,
+		b = tonumber(hex:sub(5, 6), 16) / 255
+	}
+end
+
+function utils.pixel_function(_, _, r, g, b, a)
+	local mask = utils._TC
+
+	if r == mask.r and
+		g == mask.g and
+		b == mask.b then
+		return r, g, b, 0
+	end
+
+	return r, g, b, a
+end
+
+function utils.fix_transparent_color(tileset, path)
+	local image_data = love.image.newImageData(path)
+	tileset.image = love.graphics.newImage(image_data)
+
+	if tileset.transparentcolor then
+		utils._TC = utils.hex_to_color(tileset.transparentcolor)
+
+		image_data:mapPixel(utils.pixel_function)
+		tileset.image = love.graphics.newImage(image_data)
+	end
+end
+
+function utils.deepCopy(t)
+	local copy = {}
+	for k,v in pairs(t) do
+		if type(v) == "table" then
+			v = utils.deepCopy(v)
+		end
+		copy[k] = v
+	end
+	return copy
+end
+
+return utils