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