diff --git a/.gitignore b/.gitignore
index 796b96d..29e9018 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/build
+*.tiled-session
diff --git a/src/conf.lua b/src/conf.lua
index 02cf689..e0eb56c 100644
--- a/src/conf.lua
+++ b/src/conf.lua
@@ -3,8 +3,8 @@ function love.conf(t)
t.console = true
- t.window.width = 1280/2
- t.window.height = 720/2
+ t.window.width = 854
+ t.window.height = 480
t.window.resizable = true
t.modules.joystick = false
diff --git a/src/data/main.tiled-project b/src/data/main.tiled-project
new file mode 100644
index 0000000..d58954a
--- /dev/null
+++ b/src/data/main.tiled-project
@@ -0,0 +1,11 @@
+{
+ "automappingRulesFile": "",
+ "commands": [
+ ],
+ "extensionsPath": "extensions",
+ "folders": [
+ "."
+ ],
+ "propertyTypes": [
+ ]
+}
diff --git a/src/data/maps/test.lua b/src/data/maps/test.lua
new file mode 100644
index 0000000..cb38674
--- /dev/null
+++ b/src/data/maps/test.lua
@@ -0,0 +1,79 @@
+return {
+ version = "1.9",
+ luaversion = "5.1",
+ tiledversion = "1.9.0",
+ class = "",
+ orientation = "orthogonal",
+ renderorder = "right-down",
+ width = 30,
+ height = 20,
+ tilewidth = 16,
+ tileheight = 16,
+ nextlayerid = 3,
+ nextobjectid = 1,
+ properties = {},
+ tilesets = {
+ {
+ name = "test",
+ firstgid = 1,
+ filename = "../tilesets/test.tsx"
+ }
+ },
+ layers = {
+ {
+ type = "tilelayer",
+ x = 0,
+ y = 0,
+ width = 30,
+ height = 20,
+ id = 1,
+ name = "walls",
+ class = "",
+ visible = true,
+ opacity = 1,
+ offsetx = 0,
+ offsety = 0,
+ parallaxx = 1,
+ parallaxy = 1,
+ properties = {},
+ encoding = "lua",
+ data = {
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483649, 2147483649, 2147483649, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2147483649, 2147483649, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2147483649, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2147483649, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2147483649, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2147483649, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2147483649, 2147483649, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 2147483649, 2147483649, 2147483649, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ }
+ },
+ {
+ type = "objectgroup",
+ draworder = "topdown",
+ id = 2,
+ name = "spawnpoints",
+ class = "",
+ visible = true,
+ opacity = 1,
+ offsetx = 0,
+ offsety = 0,
+ parallaxx = 1,
+ parallaxy = 1,
+ properties = {},
+ objects = {}
+ }
+ }
+}
diff --git a/src/data/maps/test.tmx b/src/data/maps/test.tmx
new file mode 100644
index 0000000..5402b5d
--- /dev/null
+++ b/src/data/maps/test.tmx
@@ -0,0 +1,32 @@
+
+
diff --git a/src/data/tilesets/test.aseprite b/src/data/tilesets/test.aseprite
new file mode 100644
index 0000000..8e85aa9
Binary files /dev/null and b/src/data/tilesets/test.aseprite differ
diff --git a/src/data/tilesets/test.lua b/src/data/tilesets/test.lua
new file mode 100644
index 0000000..0f3f9d3
--- /dev/null
+++ b/src/data/tilesets/test.lua
@@ -0,0 +1,31 @@
+return {
+ version = "1.9",
+ luaversion = "5.1",
+ tiledversion = "1.9.0",
+ name = "test",
+ class = "",
+ tilewidth = 16,
+ tileheight = 16,
+ spacing = 0,
+ margin = 0,
+ columns = 6,
+ image = "test.png",
+ imagewidth = 96,
+ imageheight = 96,
+ objectalignment = "unspecified",
+ tilerendersize = "tile",
+ fillmode = "stretch",
+ tileoffset = {
+ x = 0,
+ y = 0
+ },
+ grid = {
+ orientation = "orthogonal",
+ width = 16,
+ height = 16
+ },
+ properties = {},
+ wangsets = {},
+ tilecount = 36,
+ tiles = {}
+}
diff --git a/src/data/tilesets/test.png b/src/data/tilesets/test.png
new file mode 100644
index 0000000..1404b98
Binary files /dev/null and b/src/data/tilesets/test.png differ
diff --git a/src/data/tilesets/test.tsx b/src/data/tilesets/test.tsx
new file mode 100644
index 0000000..bae548a
--- /dev/null
+++ b/src/data/tilesets/test.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/lib/center.lua b/src/lib/center.lua
new file mode 100644
index 0000000..75b2e7d
--- /dev/null
+++ b/src/lib/center.lua
@@ -0,0 +1,138 @@
+--[[
+ MIT License
+
+ Copyright (c) 2019 Semyon Entsov
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+]]--
+
+local center = {}
+
+function center:setupScreen(width, height)
+ self._WIDTH = width
+ self._HEIGHT = height
+ self._MAX_WIDTH = 0
+ self._MAX_HEIGHT = 0
+ self._MAX_RELATIVE_WIDTH = 0
+ self._MAX_RELATIVE_HEIGHT = 0
+ self._SCREEN_WIDTH = love.graphics.getWidth()
+ self._SCREEN_HEIGHT = love.graphics.getHeight()
+ self._BORDERS = {
+ ['t'] = 0,
+ ['r'] = 0,
+ ['b'] = 0,
+ ['l'] = 0
+ }
+ self:apply()
+ return self
+end
+
+function center:setBorders(top, right, bottom, left)
+ self._BORDERS.t = top
+ self._BORDERS.r = right
+ self._BORDERS.b = bottom
+ self._BORDERS.l = left
+end
+
+function center:getScale()
+ return self._SCALE
+end
+
+function center:getOffsetX()
+ return self._OFFSET_X
+end
+
+function center:getOffsetY()
+ return self._OFFSET_Y
+end
+
+function center:setMaxWidth(width)
+ self._MAX_WIDTH = width
+end
+
+function center:setMaxHeight(height)
+ self._MAX_HEIGHT = height
+end
+
+function center:setMaxRelativeWidth(width)
+ self._MAX_RELATIVE_WIDTH = width
+end
+
+function center:setMaxRelativeHeight(height)
+ self._MAX_RELATIVE_HEIGHT = height
+end
+
+function center:resize(width, height)
+ self._SCREEN_WIDTH = width
+ self._SCREEN_HEIGHT = height
+ self:apply()
+end
+
+function center:apply()
+ local available_width = self._SCREEN_WIDTH - self._BORDERS.l - self._BORDERS.r
+ local available_height = self._SCREEN_HEIGHT - self._BORDERS.t - self._BORDERS.b
+ local max_width = available_width
+ local max_height = available_height
+ if self._MAX_RELATIVE_WIDTH > 0 and available_width * self._MAX_RELATIVE_WIDTH < max_width then
+ max_width = available_width * self._MAX_RELATIVE_WIDTH
+ end
+ if self._MAX_RELATIVE_HEIGHT > 0 and available_height * self._MAX_RELATIVE_HEIGHT < max_height then
+ max_height = available_height * self._MAX_RELATIVE_HEIGHT
+ end
+ if self._MAX_WIDTH > 0 and self._MAX_WIDTH < max_width then
+ max_width = self._MAX_WIDTH
+ end
+ if self._MAX_HEIGHT > 0 and self._MAX_HEIGHT < max_height then
+ max_height = self._MAX_HEIGHT
+ end
+ if max_height / max_width > self._HEIGHT / self._WIDTH then
+ self._CANVAS_WIDTH = max_width
+ self._CANVAS_HEIGHT = self._CANVAS_WIDTH * (self._HEIGHT / self._WIDTH)
+ else
+ self._CANVAS_HEIGHT = max_height
+ self._CANVAS_WIDTH = self._CANVAS_HEIGHT * (self._WIDTH / self._HEIGHT)
+ end
+ self._SCALE = self._CANVAS_HEIGHT / self._HEIGHT
+ self._OFFSET_X = self._BORDERS.l + (available_width - self._CANVAS_WIDTH) / 2
+ self._OFFSET_Y = self._BORDERS.t + (available_height - self._CANVAS_HEIGHT) / 2
+end
+
+function center:start()
+ love.graphics.push()
+ love.graphics.translate(self._OFFSET_X, self._OFFSET_Y)
+ love.graphics.scale(self._SCALE, self._SCALE)
+end
+
+function center:finish()
+ love.graphics.pop()
+end
+
+function center:toGame(x, y, w, h)
+ if not (self._OFFSET_X and self._OFFSET_Y and self._SCALE) then
+ return x, y, w, h
+ end
+
+ return (x - self._OFFSET_X) / self._SCALE,
+ (y - self._OFFSET_Y) / self._SCALE,
+ w and w / self._SCALE,
+ h and h / self._SCALE
+end
+
+return center
+
diff --git a/src/lib/sti/graphics.lua b/src/lib/sti/graphics.lua
new file mode 100644
index 0000000..6acf8d6
--- /dev/null
+++ b/src/lib/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
diff --git a/src/lib/sti/init.lua b/src/lib/sti/init.lua
new file mode 100644
index 0000000..9fecdab
--- /dev/null
+++ b/src/lib/sti/init.lua
@@ -0,0 +1,1627 @@
+--- 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)
+
+ -- Load possibly external tilesets
+ if map.tilesets then
+ for i, tileset in ipairs(map.tilesets) do
+ if tileset.filename then
+ local filename = utils.format_path(dir .. tileset.filename):gsub("%.tsx$", ".lua")
+ local external = assert(love.filesystem.load(filename))()
+ external.image = tileset.filename:gsub("[^/]+$", "") .. external.image
+ external.filename = filename
+ external.firstgid = tileset.firstgid
+ external.name = tileset.name
+ map.tilesets[i] = external
+ end
+ end
+ end
+ 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()
+ for _, layer in ipairs(self.layers) do
+ if layer.visible and layer.opacity > 0 then
+ self:drawLayer(layer)
+ end
+ end
+ lg.pop()
+
+ lg.push()
+ lg.translate(math.floor(tx or 0), math.floor(ty or 0))
+ 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)
diff --git a/src/lib/sti/plugins/box2d.lua b/src/lib/sti/plugins/box2d.lua
new file mode 100644
index 0000000..c6d4148
--- /dev/null
+++ b/src/lib/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
diff --git a/src/lib/sti/plugins/bump.lua b/src/lib/sti/plugins/bump.lua
new file mode 100644
index 0000000..1d4b828
--- /dev/null
+++ b/src/lib/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
diff --git a/src/lib/sti/utils.lua b/src/lib/sti/utils.lua
new file mode 100644
index 0000000..95e857a
--- /dev/null
+++ b/src/lib/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
diff --git a/src/main.lua b/src/main.lua
index 257cc57..fe212ba 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -1,6 +1,7 @@
local Gamestate = require("lib.hump.gamestate")
local binser = require("lib.binser")
local Vec = require("lib.brinevector")
+require("screen-scaler")
binser.registerStruct("brinevector",
function(v) return v.x, v.y end,
@@ -12,6 +13,6 @@ function love.load()
love.keyboard.setKeyRepeat(true)
math.randomseed(love.timer.getTime())
- Gamestate.switch(require("states.main-menu"))
+ Gamestate.switch(require("states.main"))
Gamestate.registerEvents()
end
diff --git a/src/screen-scaler.lua b/src/screen-scaler.lua
new file mode 100644
index 0000000..81301e2
--- /dev/null
+++ b/src/screen-scaler.lua
@@ -0,0 +1,254 @@
+--- Module resposible for scaling screen and centering contents
+-- while respecting ratios.
+-- @module ScreenScaler
+local ScreenScaler = {}
+
+local canvas
+
+-- This "Center" library will do most of the heavy lifting
+local Center = require("lib.center")
+
+local getRealDimensions = love.graphics.getDimensions
+
+--- Set the "ideal" dimensions from which everything else will be scaled.
+-- @tparam number w virtual width
+-- @tparam number h virtual height
+function ScreenScaler.setVirtualDimensions(w, h)
+ assert(type(w) == "number", "Expected width to be number")
+ assert(type(h) == "number", "Expected height to be number")
+ ScreenScaler.width, ScreenScaler.height = nil, nil
+
+ -- Setup library resposible for scaling the screen
+ Center:setupScreen(w, h)
+ canvas = love.graphics.newCanvas(w, h)
+ canvas:setFilter("nearest", "nearest")
+
+ ScreenScaler.width, ScreenScaler.height = w, h
+end
+
+--- Unsets virtual dimensions. Effectively disables scaler.
+function ScreenScaler.unsetVirtualDimensions()
+ ScreenScaler.width = nil
+ ScreenScaler.height = nil
+end
+
+--- Get virtual dimensions.
+-- @return width, height
+function ScreenScaler.getVirtualDimensions()
+ return ScreenScaler.width, ScreenScaler.height
+end
+
+--- Returns true if scaler is enabled.
+-- @treturn boolean
+function ScreenScaler.isEnabled()
+ return ScreenScaler.width ~= nil
+end
+
+-- Overwrite default love.mouse functions to return position
+-- relative to scaled window
+do
+ local getX = love.mouse.getX
+ function love.mouse.getX()
+ local x = getX()
+ if not ScreenScaler.isEnabled() then
+ return x
+ end
+ return (x - Center:getOffsetX()) / Center:getScale()
+ end
+
+ local getY = love.mouse.getY
+ function love.mouse.getY()
+ local y = getY()
+ if not ScreenScaler.isEnabled() then
+ return y
+ end
+ return (y - Center:getOffsetY()) / Center:getScale()
+ end
+
+ local getPosition = love.mouse.getPosition
+ function love.mouse.getPosition()
+ if ScreenScaler.isEnabled() then
+ return Center:toGame(getPosition())
+ else
+ return getPosition()
+ end
+ end
+
+ -- TODO: add replacements for setX, setY, setPosition
+end
+
+-- Overwrite default getDimensions, getWidth, getHeight
+-- to return virtual width and height if scaler is enabled
+do
+ local getWidth = love.graphics.getWidth
+ function love.graphics.getWidth()
+ return ScreenScaler.width or getWidth()
+ end
+
+ local getHeight = love.graphics.getHeight
+ function love.graphics.getHeight()
+ return ScreenScaler.height or getHeight()
+ end
+
+ function love.graphics.getDimensions()
+ return love.graphics.getWidth(), love.graphics.getHeight()
+ end
+end
+
+-- Adjust setScissor and intersectScissor function, so that they are relative
+-- to the scaled screen. By default these functions are unaffected by transformations
+do
+ local setScissor = love.graphics.setScissor
+ function love.graphics.setScissor(x, y, w, h)
+ if x and ScreenScaler.isEnabled() then
+ setScissor(Center:toGame(x, y, w, h))
+ else
+ setScissor(x, y, w, h)
+ end
+ end
+
+ local intersectScissor = love.graphics.intersectScissor
+ function love.graphics.intersectScissor(x, y, w, h)
+ if x and ScreenScaler.isEnabled() then
+ intersectScissor(Center:toGame(x, y, w, h))
+ else
+ intersectScissor(x, y, w, h)
+ end
+ end
+end
+
+local function isInBounds(x, y)
+ if not ScreenScaler.isEnabled() then return true end
+ local w, h = ScreenScaler.getVirtualDimensions()
+ return x >= 0 and x < w and y >= 0 and y < h
+end
+
+-- Create event proccessors for converting normal screen coordinates
+-- to scaled screen coordinates
+-- If the user clicked out of bounds, it will not handled
+local eventPreProccessor = {}
+function eventPreProccessor.mousepressed(x, y, button, istouch, presses)
+ x, y = Center:toGame(x, y)
+ return isInBounds(x, y), x, y, button, istouch, presses
+end
+
+function eventPreProccessor.mousereleased(x, y, button, istouch, presses)
+ x, y = Center:toGame(x, y)
+ return isInBounds(x, y), x, y, button, istouch, presses
+end
+
+function eventPreProccessor.mousemoved(x, y, dx, dy, istouch)
+ local scale = Center:getScale()
+ x, y = Center:toGame(x, y)
+ dx, dy = dx / scale, dy / scale
+ return isInBounds(x, y), x, y, dx, dy, istouch, istouch
+end
+
+function eventPreProccessor.wheelmoved(x, y)
+ return isInBounds(love.mouse.getPosition()), x, y
+end
+
+local function hideOutOfBounds()
+ local r, g, b, a = love.graphics.getColor()
+ love.graphics.setColor(0, 0, 0, 1)
+ local w, h = getRealDimensions()
+
+ if Center._OFFSET_X ~= 0 then
+ love.graphics.rectangle("fill", 0, 0, Center._OFFSET_X, h)
+ love.graphics.rectangle("fill", Center._WIDTH*Center._SCALE+Center._OFFSET_X, 0, Center._OFFSET_X, h)
+ end
+
+ if Center._OFFSET_Y ~= 0 then
+ love.graphics.rectangle("fill", 0, 0, w, Center._OFFSET_Y)
+ love.graphics.rectangle("fill", 0, Center._HEIGHT*Center._SCALE+Center._OFFSET_Y, w, Center._OFFSET_Y)
+ end
+
+ love.graphics.setColor(r, g, b, a)
+end
+
+-- Modify core game loop so that if scaler is enabled:
+-- * resize events are not handled
+-- * out of bounds mouse events are not handled
+-- * all drawing operations are centered
+function love.run()
+ if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
+
+ -- We don't want the first frame's dt to include time taken by love.load.
+ if love.timer then love.timer.step() end
+
+ local dt = 0
+
+ -- Main loop time.
+ return function()
+ -- Process events.
+ if love.event then
+ love.event.pump()
+ for name, a,b,c,d,e,f in love.event.poll() do
+ if name == "quit" then
+ if not love.quit or not love.quit() then
+ return a or 0
+ end
+ end
+
+ if ScreenScaler.isEnabled() then
+ if name == "resize" then
+ Center:resize(a, b)
+ goto continue
+ elseif eventPreProccessor[name] then
+ local success
+ success, a, b, c, d, e, f = eventPreProccessor[name](a, b, c, d, e, f)
+ if not success then goto continue end
+ end
+ end
+ love.handlers[name](a, b, c, d, e, f)
+ ::continue::
+ end
+ end
+
+ -- Update dt, as we'll be passing it to update
+ if love.timer then dt = love.timer.step() end
+
+ -- Call update and draw
+ if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
+
+ if love.graphics and love.graphics.isActive() then
+ love.graphics.origin()
+
+ if canvas then
+ love.graphics.clear(love.graphics.getBackgroundColor())
+
+ love.graphics.setCanvas(canvas)
+ love.graphics.clear(love.graphics.getBackgroundColor())
+ love.draw()
+ love.graphics.setCanvas()
+
+ Center:start()
+ love.graphics.draw(canvas)
+ Center:finish()
+ else
+ love.graphics.clear(love.graphics.getBackgroundColor())
+
+ if love.draw then
+ if ScreenScaler.isEnabled() then
+ love.graphics.setCanvas(canvas)
+ love.draw()
+ love.graphics.setCanvas()
+
+ Center:start()
+ love.graphics.draw(canvas)
+ Center:finish()
+ hideOutOfBounds()
+ else
+ love.draw()
+ end
+ end
+ end
+
+ love.graphics.present()
+ end
+
+ if love.timer then love.timer.sleep(0.001) end
+ end
+end
+
+return ScreenScaler
diff --git a/src/states/main.lua b/src/states/main.lua
index 5598935..7c817db 100644
--- a/src/states/main.lua
+++ b/src/states/main.lua
@@ -4,24 +4,32 @@ local nata = require("lib.nata")
local data = require("data")
function MainState:enter(_, host_socket)
+ local groups = {
+ physical = {filter = {"pos", "vel"}},
+ player = {filter = {
+ "pos", "acc", "speed",
+ "bolt_count",
+ "bolt_cooldown", "bolt_speed", "bolt_friction"
+ }},
+ controllable_player = {filter = {"controllable"}},
+ sprite = {filter = {"sprite"}},
+ bolt = {filter={"pos", "vel", "bolt"}}
+ }
+
+ local systems = {
+ require("systems.physics"),
+ require("systems.map"),
+ require("systems.player"),
+ require("systems.sprite"),
+ }
+
+ if host_socket then
+ table.insert(systems, require("systems.multiplayer"))
+ end
+
self.ecs = nata.new{
- groups = {
- physical = {filter = {"pos", "vel"}},
- player = {filter = {
- "pos", "acc", "speed",
- "bolt_count",
- "bolt_cooldown", "bolt_speed", "bolt_friction"
- }},
- controllable_player = {filter = {"controllable"}},
- sprite = {filter = {"sprite"}},
- bolt = {filter={"pos", "vel", "bolt"}}
- },
- systems = {
- require("systems.physics"),
- require("systems.player"),
- require("systems.sprite"),
- require("systems.multiplayer")
- },
+ groups = groups,
+ systems = systems,
data = {
host_socket = host_socket
}
@@ -38,8 +46,10 @@ function MainState:update(dt)
end
function MainState:cleanup()
- local MultiplayerSystem = self.ecs:getSystem(require("systems.multiplayer"))
- MultiplayerSystem:disconnectPeers()
+ local multiplayer = self.ecs:getSystem(require("systems.multiplayer"))
+ if multiplayer then
+ multiplayer:disconnectPeers()
+ end
end
function MainState:quit()
diff --git a/src/systems/map.lua b/src/systems/map.lua
new file mode 100644
index 0000000..3949719
--- /dev/null
+++ b/src/systems/map.lua
@@ -0,0 +1,37 @@
+local rgb = require("helpers.rgb")
+local sti = require("lib.sti")
+local ScreenScaler = require("screen-scaler")
+local Map = {}
+
+local DEBUG_GRID = true
+local DEBUG_GRID_COLOR = rgb(30, 30, 30)
+
+function Map:init()
+ self.map = sti("data/maps/test.lua")
+
+ ScreenScaler.setVirtualDimensions(
+ self.map.width * self.map.tilewidth,
+ self.map.height * self.map.tileheight
+ )
+end
+
+function Map:update(dt)
+ self.map:update(dt)
+end
+
+function Map:draw()
+ if DEBUG_GRID and self.map then
+ local w, h = love.graphics.getDimensions()
+ love.graphics.setColor(DEBUG_GRID_COLOR)
+ for x=0, w, self.map.tilewidth do
+ love.graphics.line(x, 0, x, h)
+ end
+ for y=0, h, self.map.tileheight do
+ love.graphics.line(0, y, w, y)
+ end
+ end
+ love.graphics.setColor(1, 1, 1)
+ self.map:draw()
+end
+
+return Map
diff --git a/src/systems/player.lua b/src/systems/player.lua
index e64b748..0dc9c55 100644
--- a/src/systems/player.lua
+++ b/src/systems/player.lua
@@ -142,8 +142,8 @@ function Player:draw()
for _, e in ipairs(self.pool.groups.player.entities) do
if e.aim_dir.x ~= 0 or e.aim_dir.y ~= 0 then
love.graphics.setColor(0.8, 0.2, 0.2)
- local aim_pos = e.pos + e.aim_dir * 25
- love.graphics.circle("fill", aim_pos.x, aim_pos.y, 10)
+ local aim_pos = e.pos + e.aim_dir * 15
+ love.graphics.circle("fill", aim_pos.x, aim_pos.y, 5)
end
end
end
diff --git a/src/systems/sprite.lua b/src/systems/sprite.lua
index 9336b23..5945f58 100644
--- a/src/systems/sprite.lua
+++ b/src/systems/sprite.lua
@@ -3,7 +3,7 @@ local Sprite = {}
function Sprite:draw()
for _, e in ipairs(self.pool.groups.sprite.entities) do
love.graphics.setColor(1, 1, 1)
- love.graphics.circle("fill", e.pos.x, e.pos.y, 20)
+ love.graphics.circle("fill", e.pos.x, e.pos.y, 10)
end
end