diff --git a/examples/gfx/border.bmp b/examples/gfx/border.bmp new file mode 100644 index 0000000..f833594 Binary files /dev/null and b/examples/gfx/border.bmp differ diff --git a/examples/gfx/border_NRM.png b/examples/gfx/border_NRM.png new file mode 100644 index 0000000..80ab4b8 Binary files /dev/null and b/examples/gfx/border_NRM.png differ diff --git a/examples/gfx/map.lua b/examples/gfx/map.lua new file mode 100644 index 0000000..59a93ae --- /dev/null +++ b/examples/gfx/map.lua @@ -0,0 +1,61 @@ +return { + version = "1.1", + luaversion = "5.1", + orientation = "orthogonal", + width = 25, + height = 15, + tilewidth = 32, + tileheight = 32, + backgroundcolor = { 128, 128, 128, 0 }, + properties = {}, + tilesets = { + { + name = "base", + firstgid = 1, + tilewidth = 32, + tileheight = 32, + spacing = 0, + margin = 0, + image = "border.bmp", + imagewidth = 32, + imageheight = 32, + tileoffset = { + x = 0, + y = 0 + }, + properties = {}, + tiles = {} + } + }, + layers = { + { + type = "tilelayer", + name = "Tile Layer 1", + x = 0, + y = 0, + width = 25, + height = 15, + visible = true, + opacity = 1, + properties = {}, + encoding = "lua", + data = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 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, 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, 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, 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, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, + 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, + 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 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, 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, 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, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + } + } + } +} diff --git a/examples/gfx/map.tmx b/examples/gfx/map.tmx new file mode 100644 index 0000000..3eea369 --- /dev/null +++ b/examples/gfx/map.tmx @@ -0,0 +1,11 @@ + + + + + + + + eJxjZGBgYBxGmFZg1A7K7SA2roiJ36FgBzFgIO2gJhi1gzw7aIkB2WcAjg== + + + diff --git a/examples/simple_tiled_impl.lua b/examples/simple_tiled_impl.lua new file mode 100644 index 0000000..782c064 --- /dev/null +++ b/examples/simple_tiled_impl.lua @@ -0,0 +1,79 @@ +-- Example: STI Example +local LightWorld = require "lib" +local sti = require 'examples.vendor.sti' + +function love.load() + x = 0 + y = 0 + z = 1 + scale = 1 + + -- create light world + lightWorld = LightWorld({ + ambient = {55,55,55}, + }) + + map = sti.new("examples/gfx/map") + image_normal = love.graphics.newImage("examples/gfx/border_NRM.png") + + -- create light + lightMouse = lightWorld:newLight(0, 0, 255, 127, 63, 300) + lightMouse:setGlowStrength(0.3) + + -- walls + lightWorld:newRectangle(400, 32, 800, 64):setNormalMap(image_normal, 800, 64) + lightWorld:newRectangle(32, 272, 64, 416):setNormalMap(image_normal, 64, 416) + lightWorld:newRectangle(400, 464, 800, 32):setNormalMap(image_normal, 800, 32) + lightWorld:newRectangle(784, 272, 32, 416):setNormalMap(image_normal, 32, 416) + + --blocks + lightWorld:newRectangle(224, 256, 128, 124):setNormalMap(image_normal, 128, 124) + lightWorld:newRectangle(592, 224, 224, 64):setNormalMap(image_normal, 224, 64) +end + +function love.update(dt) + love.window.setTitle("Light vs. Shadow Engine (FPS:" .. love.timer.getFPS() .. ")") + + if love.keyboard.isDown("down") then + y = y - dt * 200 + elseif love.keyboard.isDown("up") then + y = y + dt * 200 + end + + if love.keyboard.isDown("right") then + x = x - dt * 200 + elseif love.keyboard.isDown("left") then + x = x + dt * 200 + end + + if love.keyboard.isDown("-") then + scale = scale - 0.01 + elseif love.keyboard.isDown("=") then + scale = scale + 0.01 + end + + map:update(dt) + lightMouse:setPosition((love.mouse.getX() - x)/scale, (love.mouse.getY() - y)/scale, z) +end + +function love.mousepressed(x, y, c) + if c == "wu" then + z = z + 1 + elseif c == "wd" then + z = z - 1 + end +end + +function love.draw() + lightWorld:setTranslation(x, y, scale) + love.graphics.push() + love.graphics.translate(x, y) + love.graphics.scale(scale) + lightWorld:draw(function() + love.graphics.setColor(255, 255, 255) + love.graphics.rectangle("fill", -x/scale, -y/scale, love.graphics.getWidth()/scale, love.graphics.getHeight()/scale) + map:draw() + end) + love.graphics.pop() + +end diff --git a/examples/vendor/sti/.gitattributes b/examples/vendor/sti/.gitattributes new file mode 100755 index 0000000..412eeda --- /dev/null +++ b/examples/vendor/sti/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/examples/vendor/sti/.gitignore b/examples/vendor/sti/.gitignore new file mode 100755 index 0000000..b9d6bd9 --- /dev/null +++ b/examples/vendor/sti/.gitignore @@ -0,0 +1,215 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/examples/vendor/sti/framework/corona.lua b/examples/vendor/sti/framework/corona.lua new file mode 100755 index 0000000..d469ffd --- /dev/null +++ b/examples/vendor/sti/framework/corona.lua @@ -0,0 +1,56 @@ +local lf = love.filesystem +local lg = love.graphics +local lm = love.math +local lp = love.physics +local framework = {} + +framework.version = "Corona" + +function framework.load(file) + return assert(lf.load(file), "File not found: " .. file) +end + +function framework.newImage(path) + local image = lg.newImage(path) + image:setFilter("nearest", "nearest") + + return image +end + +function framework:newCanvas(w, h) + w = w or self.getWidth() + h = h or self.getHeight() + local canvas = lg.newCanvas(w, h) + canvas:setFilter("nearest", "nearest") + + return canvas +end + +-- Graphics Calls +framework.clear = lg.clear +framework.draw = lg.draw +framework.getHeight = lg.getHeight +framework.getWidth = lg.getWidth +framework.line = lg.line +framework.newSpriteBatch = lg.newSpriteBatch +framework.newQuad = lg.newQuad +framework.polygon = lg.polygon +framework.rectangle = lg.rectangle +framework.setColor = lg.setColor +framework.setCanvas = lg.setCanvas +framework.origin = lg.origin +framework.pop = lg.pop +framework.push = lg.push + +-- Math Calls +framework.isConvex = lm.isConvex +framework.triangulate = lm.triangulate + +-- Physics Calls +framework.getMeter = lp.getMeter +framework.newBody = lp.newBody +framework.newChainShape = lp.newChainShape +framework.newFixture = lp.newFixture +framework.newPolygonShape = lp.newPolygonShape + +return framework diff --git a/examples/vendor/sti/framework/love.lua b/examples/vendor/sti/framework/love.lua new file mode 100755 index 0000000..7f49443 --- /dev/null +++ b/examples/vendor/sti/framework/love.lua @@ -0,0 +1,62 @@ +local lf = love.filesystem +local lg = love.graphics +local lm = love.math +local lp = love.physics +local framework = {} + +framework.version = "LOVE" + +assert(lf, "The love.filesystem module is required for this library.") +assert(lg, "The love.graphics module is required for this library.") +assert(lm, "The love.math module is required for this library.") + +function framework.load(file) + return assert(lf.load(file), "File not found: " .. file) +end + +function framework.newImage(path) + local image = lg.newImage(path) + image:setFilter("nearest", "nearest") + + return image +end + +function framework:newCanvas(w, h) + w = w or self.getWidth() + h = h or self.getHeight() + local canvas = lg.newCanvas(w, h) + canvas:setFilter("nearest", "nearest") + + return canvas +end + +-- Graphics Calls +framework.draw = lg.draw +framework.getCanvas = lg.getCanvas +framework.getHeight = lg.getHeight +framework.getWidth = lg.getWidth +framework.line = lg.line +framework.newSpriteBatch = lg.newSpriteBatch +framework.newQuad = lg.newQuad +framework.polygon = lg.polygon +framework.rectangle = lg.rectangle +framework.setColor = lg.setColor +framework.setCanvas = lg.setCanvas +framework.origin = lg.origin +framework.pop = lg.pop +framework.push = lg.push + +-- Math Calls +framework.isConvex = lm.isConvex +framework.triangulate = lm.triangulate + +-- Physics Calls +if lp then + framework.getMeter = lp.getMeter + framework.newBody = lp.newBody + framework.newChainShape = lp.newChainShape + framework.newFixture = lp.newFixture + framework.newPolygonShape = lp.newPolygonShape +end + +return framework diff --git a/examples/vendor/sti/framework/pure.lua b/examples/vendor/sti/framework/pure.lua new file mode 100755 index 0000000..18bdf47 --- /dev/null +++ b/examples/vendor/sti/framework/pure.lua @@ -0,0 +1,56 @@ +local lf = love.filesystem +local lg = love.graphics +local lm = love.math +local lp = love.physics +local framework = {} + +framework.version = "Lua" + +function framework.load(file) + return assert(lf.load(file), "File not found: " .. file) +end + +function framework.newImage(path) + local image = lg.newImage(path) + image:setFilter("nearest", "nearest") + + return image +end + +function framework:newCanvas(w, h) + w = w or self.getWidth() + h = h or self.getHeight() + local canvas = lg.newCanvas(w, h) + canvas:setFilter("nearest", "nearest") + + return canvas +end + +-- Graphics Calls +framework.clear = lg.clear +framework.draw = lg.draw +framework.getHeight = lg.getHeight +framework.getWidth = lg.getWidth +framework.line = lg.line +framework.newSpriteBatch = lg.newSpriteBatch +framework.newQuad = lg.newQuad +framework.polygon = lg.polygon +framework.rectangle = lg.rectangle +framework.setColor = lg.setColor +framework.setCanvas = lg.setCanvas +framework.origin = lg.origin +framework.pop = lg.pop +framework.push = lg.push + +-- Math Calls +framework.isConvex = lm.isConvex +framework.triangulate = lm.triangulate + +-- Physics Calls +framework.getMeter = lp.getMeter +framework.newBody = lp.newBody +framework.newChainShape = lp.newChainShape +framework.newFixture = lp.newFixture +framework.newPolygonShape = lp.newPolygonShape + +return framework diff --git a/examples/vendor/sti/init.lua b/examples/vendor/sti/init.lua new file mode 100755 index 0000000..a1bc510 --- /dev/null +++ b/examples/vendor/sti/init.lua @@ -0,0 +1,90 @@ +--[[ +------------------------------------------------------------------------------ +Simple Tiled Implementation is licensed under the MIT Open Source License. +(http://www.opensource.org/licenses/mit-license.html) +------------------------------------------------------------------------------ + +Copyright (c) 2014 Landon Manning - LManning17@gmail.com - LandonManning.com + +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 STI = { + _LICENSE = "STI is distributed under the terms of the MIT license. See LICENSE.md.", + _URL = "https://github.com/karai17/Simple-Tiled-Implementation", + _VERSION = "0.9.4", + _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework." +} + +local path = ... .. "." -- lol +local Map = require(path .. "map") +local framework + +if love then + framework = require(path .. "framework.love") +elseif corona then -- I don't think this works + framework = require(path .. "framework.corona") +else + framework = require(path .. "framework.pure") +end + +function STI.new(map) + map = map .. ".lua" + + -- Get path to map + local path = map:reverse():find("[/\\]") or "" + if path ~= "" then + path = map:sub(1, 1 + (#map - path)) + end + + -- Load map + map = framework.load(map) + setfenv(map, {}) + map = setmetatable(map(), {__index = Map}) + + map:init(path, framework) + + return map +end + +-- http://wiki.interfaceware.com/534.html +function string.split(s, d) + local t = {} + local i = 0 + local f + local match = '(.-)' .. d .. '()' + + if string.find(s, d) == nil then + return {s} + end + + for sub, j in string.gmatch(s, match) do + i = i + 1 + t[i] = sub + f = j + end + + if i ~= 0 then + t[i+1] = string.sub(s, f) + end + + return t +end + +return STI diff --git a/examples/vendor/sti/map.lua b/examples/vendor/sti/map.lua new file mode 100755 index 0000000..0dfbe15 --- /dev/null +++ b/examples/vendor/sti/map.lua @@ -0,0 +1,1085 @@ +local Map = {} +local framework + +-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286 +local function formatPath(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,'/') + until k == 0 + + repeat -- A/../ -> (empty) + path,k = path:gsub(np_pat1,'') + until k == 0 + + if path == '' then path = '.' end + + return path +end + +local function rotateVertex(v, x, y, cos, sin) + local vertex = { + x = v.x, + y = v.y, + } + + vertex.x = vertex.x - x + vertex.y = vertex.y - y + + local vx = cos * vertex.x - sin * vertex.y + local vy = sin * vertex.x + cos * vertex.y + + return vx + x, vy + y +end + +local function convertEllipseToPolygon(x, y, w, h, max_segments) + 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, math.ceil(segments/4-1), math.ceil(segments/4) } + + local m + if framework.getMeter then + m = framework.getMeter() + else + m = self.tilewidth + self.tileheight / 2 + end + + for _, i in ipairs(v) do + local angle = (i / segments) * math.pi * 2 + local px = x + w / 2 + math.cos(angle) * w / 2 + local py = y + h / 2 + math.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 hard-coded 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 + math.cos(angle) * w / 2 + local py = y + h / 2 + math.sin(angle) * h / 2 + + table.insert(vertices, { x = px, y = py }) + end + + return vertices +end + +function Map:init(path, fw) + framework = fw + + self.canvas = framework:newCanvas() + self.tiles = {} + self.tileInstances = {} + self.drawRange = { + sx = 1, + sy = 1, + ex = self.width, + ey = self.height, + } + + -- Set tiles, images + local gid = 1 + for i, tileset in ipairs(self.tilesets) do + local image = formatPath(path .. tileset.image) + tileset.image = framework.newImage(image) + gid = self:setTiles(i, tileset, gid) + end + + -- Set layers + for i, layer in ipairs(self.layers) do + self:setLayer(layer, path) + end +end + +function Map:initWorldCollision(world) + assert(framework.newBody, "To use the built-in collision system, please enable the physics module.") + + local body = framework.newBody(world) + local collision = { + body = body, + } + + local function addObjectToWorld(objshape, vertices) + local shape + + if objshape == "polyline" then + shape = framework.newChainShape(false, unpack(vertices)) + else + shape = framework.newPolygonShape(unpack(vertices)) + end + + local fixture = framework.newFixture(body, shape) + local obj = { + shape = shape, + fixture = fixture, + } + + table.insert(collision, obj) + end + + local function getPolygonVertices(object, tile, precalc) + local ox, oy = 0, 0 + + if not precalc then + ox = object.x + oy = object.y + end + + local vertices = {} + for _, vertex in ipairs(object.polygon) do + table.insert(vertices, tile.x + ox + vertex.x) + table.insert(vertices, tile.y + oy + vertex.y) + end + + return vertices + end + + local function calculateObjectPosition(object, tile) + local o = { + shape = object.shape, + x = object.x, + y = object.y, + w = object.width, + h = object.height, + polygon = object.polygon or object.polyline or object.ellipse or object.rectangle + } + local t = tile or { x=0, y=0 } + + if o.shape == "rectangle" then + o.r = object.rotation or 0 + local cos = math.cos(math.rad(o.r)) + local sin = math.sin(math.rad(o.r)) + + if object.gid then + local tileset = self.tiles[object.gid].tileset + local lid = object.gid - self.tilesets[tileset].firstgid + local tile = {} + + -- This fixes a height issue + o.y = o.y + self.tiles[object.gid].offset.y + + for _, t in ipairs(self.tilesets[tileset].tiles) do + if t.id == lid then + tile = t + break + end + end + + if tile.objectGroup then + for _, obj in ipairs(tile.objectGroup.objects) do + -- Every object in the tile + calculateObjectPosition(obj, object) + end + + return + else + o.w = self.tiles[object.gid].width + o.h = self.tiles[object.gid].height + end + end + + o.polygon = { + { x=o.x, y=o.y }, + { x=o.x + o.w, y=o.y }, + { x=o.x + o.w, y=o.y + o.h }, + { x=o.x, y=o.y + o.h }, + } + + for _, vertex in ipairs(o.polygon) do + if self.orientation == "isometric" then + vertex.x, vertex.y = self:convertIsometricToScreen(vertex.x, vertex.y) + end + + vertex.x, vertex.y = rotateVertex(vertex, o.x, o.y, cos, sin) + end + + local vertices = getPolygonVertices(o, t, true) + addObjectToWorld(o.shape, vertices) + elseif o.shape == "ellipse" then + if not o.polygon then + o.polygon = convertEllipseToPolygon(o.x, o.y, o.w, o.h) + end + local vertices = getPolygonVertices(o, t, true) + local triangles = framework.triangulate(vertices) + + for _, triangle in ipairs(triangles) do + addObjectToWorld(o.shape, triangle) + end + elseif o.shape == "polygon" then + local precalc = false + if not t.gid then precalc = true end + + local vertices = getPolygonVertices(o, t, precalc) + local triangles = framework.triangulate(vertices) + + for _, triangle in ipairs(triangles) do + addObjectToWorld(o.shape, triangle) + end + elseif o.shape == "polyline" then + local precalc = false + if not t.gid then precalc = true end + + local vertices = getPolygonVertices(o, t, precalc) + addObjectToWorld(o.shape, vertices) + end + end + + for _, tileset in ipairs(self.tilesets) do + for _, tile in ipairs(tileset.tiles) do + local gid = tileset.firstgid + tile.id + + if tile.objectGroup then + if self.tileInstances[gid] then + for _, instance in ipairs(self.tileInstances[gid]) do + for _, object in ipairs(tile.objectGroup.objects) do + -- Every object in every instance of a tile + calculateObjectPosition(object, instance) + end + end + end + elseif tile.properties.collidable == "true" then + for _, instance in ipairs(self.tileInstances[gid]) do + -- Every instance of a tile + local object = { + shape = "rectangle", + x = 0, + y = 0, + width = tileset.tilewidth, + height = tileset.tileheight, + } + + calculateObjectPosition(object, instance) + end + end + end + end + + for _, layer in ipairs(self.layers) do + if layer.properties.collidable == "true" then + -- Entire layer + if layer.type == "tilelayer" then + for y, tiles in ipairs(layer.data) do + for x, tile in pairs(tiles) do + local object = { + shape = "rectangle", + x = x * self.width + tile.offset.x, + y = y * self.height + tile.offset.y, + width = tile.width, + height = tile.height, + } + calculateObjectPosition(object) + 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, + } + calculateObjectPosition(object) + end + end + + if layer.type == "objectgroup" then + for _, object in ipairs(layer.objects) do + if object.properties.collidable == "true" then + -- Individual objects + calculateObjectPosition(object) + end + end + end + end + + return collision +end + +function Map:setTiles(index, tileset, gid) + local function getTiles(i, t, m, s) + i = i - m + local n = 0 + + while i >= t do + i = i - t + if n ~= 0 then i = i - s end + if i >= 0 then n = n + 1 end + end + + return n + end + + local quad = framework.newQuad + local mw = self.tilewidth + local iw = tileset.imagewidth + local ih = tileset.imageheight + local tw = tileset.tilewidth + local th = tileset.tileheight + local s = tileset.spacing + local m = tileset.margin + local w = getTiles(iw, tw, m, s) + local h = getTiles(ih, th, m, s) + + for y = 1, h do + for x = 1, w do + local id = gid - tileset.firstgid + local qx = (x - 1) * tw + m + (x - 1) * s + local qy = (y - 1) * th + m + (y - 1) * s + local properties + local terrain + local animation + + for _, tile in pairs(tileset.tiles) do + if tile.id == id then + properties = tile.properties + animation = tile.animation + 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, + quad = quad(qx, qy, tw, th, iw, ih), + properties = properties, + terrain = terrain, + animation = animation, + frame = 1, + time = 0, + width = tileset.tilewidth, + height = tileset.tileheight, + sx = 1, + sy = 1, + r = 0, + offset = { + x = -mw + tileset.tileoffset.x, + y = -th + tileset.tileoffset.y, + }, + } + + if self.orientation == "isometric" then + tile.offset.x = -mw / 2 + end + + self.tiles[gid] = tile + gid = gid + 1 + end + end + + return gid +end + +function Map:setLayer(layer, path) + layer.x = layer.x or 0 + layer.y = layer.y or 0 + layer.update = function(dt) return 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: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 image = formatPath(path..layer.image) + layer.image = framework.newImage(image) + layer.width = layer.image:getWidth() + layer.height = layer.image:getHeight() + end + end + + self.layers[layer.name] = layer +end + +function Map:setTileData(layer) + local i = 1 + local map = {} + + for y = 1, layer.height do + map[y] = {} + for x = 1, layer.width do + local gid = layer.data[i] + + if gid > 0 then + local tile = self.tiles[gid] + + if tile then + map[y][x] = tile + else + 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 = tile.gid, + tileset = tile.tileset, + 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 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 + map[y][x] = self.tiles[gid] + end + else + map[y][x] = false + end + + i = i + 1 + end + end + + layer.data = map +end + +function Map:setObjectCoordinates(layer) + local function updateVertex(vertex, x, y, cos, sin) + if self.orientation == "isometric" then + x, y = self:convertIsometricToScreen(x, y) + vertex.x, vertex.y = self:convertIsometricToScreen(vertex.x, vertex.y) + end + + return rotateVertex(vertex, x, y, cos, sin) + end + + 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 r = object.rotation + local cos = math.cos(math.rad(r)) + local sin = math.sin(math.rad(r)) + + if object.shape == "rectangle" 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 = updateVertex(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 = convertEllipseToPolygon(x, y, w, h) + + for _, vertex in ipairs(vertices) do + vertex.x, vertex.y = updateVertex(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 = x + vertex.x + vertex.y = y + vertex.y + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + end + elseif object.shape == "polyline" then + for _, vertex in ipairs(object.polyline) do + vertex.x = x + vertex.x + vertex.y = y + vertex.y + vertex.x, vertex.y = updateVertex(vertex, x, y, cos, sin) + end + end + end +end + +function Map:setSpriteBatches(layer) + local newBatch = framework.newSpriteBatch + local w = framework.getWidth() + local h = framework.getHeight() + local tw = self.tilewidth + local th = self.tileheight + local bw = math.ceil(w / tw) + local bh = math.ceil(h / th) + + -- Minimum of 400 tiles per batch + if bw < 20 then bw = 20 end + if bh < 20 then bh = 20 end + + local size = bw * bh + local batches = { + width = bw, + height = bh, + data = {}, + } + + for y = 1, layer.height do + local by = math.ceil(y / bh) + + for x = 1, layer.width do + local tile = layer.data[y][x] + local bx = math.ceil(x / bw) + local id + + if tile then + local ts = tile.tileset + local image = self.tilesets[tile.tileset].image + + batches.data[ts] = batches.data[ts] or {} + batches.data[ts][by] = batches.data[ts][by] or {} + batches.data[ts][by][bx] = batches.data[ts][by][bx] or newBatch(image, size) + + local batch = batches.data[ts][by][bx] + local tx, ty + + if self.orientation == "orthogonal" then + tx = x * tw + tile.offset.x + ty = y * th + tile.offset.y + + -- Compensation for scale/rotation shift + if tile.sx < 0 then tx = tx + tw end + if tile.sy < 0 then ty = ty + th end + if tile.r > 0 then tx = tx + tw end + if tile.r < 0 then ty = ty + th end + elseif self.orientation == "isometric" then + tx = (x - y) * (tw / 2) + tile.offset.x + layer.width * tw / 2 + ty = (x + y) * (th / 2) + tile.offset.y + elseif self.orientation == "staggered" then + if y % 2 == 0 then + tx = x * tw + tw / 2 + tile.offset.x + else + tx = x * tw + tile.offset.x + end + + ty = y * th / 2 + tile.offset.y + th / 2 + end + + id = batch:add(tile.quad, tx, ty, tile.r, tile.sx, tile.sy) + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], { batch=batch, id=id, gid=tile.gid, x=tx, y=ty }) + end + end + end + + layer.batches = batches +end + +function Map:setObjectSpriteBatches(layer) + local newBatch = framework.newSpriteBatch + local tw = self.tilewidth + local th = self.tileheight + local batches = { + } + + for _, object in ipairs(layer.objects) do + if object.gid then + local tile = self.tiles[object.gid] + local ts = tile.tileset + local image = self.tilesets[tile.tileset].image + + batches[ts] = batches[ts] or newBatch(image, 100) + + local batch = batches[ts] + local tx = object.x + tw + tile.offset.x + local ty = object.y + tile.offset.y + + -- Compensation for scale/rotation shift + if tile.sx < 0 then tx = tx + tw end + if tile.sy < 0 then ty = ty + th end + if tile.r > 0 then tx = tx + tw end + if tile.r < 0 then ty = ty + th end + + id = batch:add(tile.quad, tx, ty, tile.r, tile.sx, tile.sy) + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], { batch=batch, id=id, gid=tile.gid, x=tx, y=ty }) + end + end + + layer.batches = batches +end + +function Map:setDrawRange(tx, ty, w, h) + tx = -tx + ty = -ty + local tw, th = self.tilewidth, self.tileheight + local sx, sy, ex, ey + + if self.orientation == "orthogonal" then + sx = math.ceil(tx / tw) + sy = math.ceil(ty / th) + ex = math.ceil(sx + w / tw) + ey = math.ceil(sy + h / th) + elseif self.orientation == "isometric" then + sx = math.ceil(((ty / (th / 2)) + (tx / (tw / 2))) / 2) + sy = math.ceil(((ty / (th / 2)) - (tx / (tw / 2))) / 2 - h / th) + ex = math.ceil(sx + (h / th) + (w / tw)) + ey = math.ceil(sy + (h / th) * 2 + (w / tw)) + elseif self.orientation == "staggered" then + sx = math.ceil(tx / tw - 1) + sy = math.ceil(ty / th) + ex = math.ceil(sx + w / tw + 1) + ey = math.ceil(sy + h / th * 2) + end + + self.drawRange = { + sx = sx, + sy = sy, + ex = ex, + ey = ey, + } +end + +function Map:addCustomLayer(name, index) + local layer = { + type = "customlayer", + name = name, + visible = true, + opacity = 1, + properties = {}, + } + + function layer:draw() return end + function layer:update(dt) return end + + table.insert(self.layers, index, layer) + self.layers[name] = self.layers[index] + + return layer +end + +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.objects = nil + layer.image = nil + + function layer:draw() return end + function layer:update(dt) return end +end + +function Map:removeLayer(index) + local layer = assert(self.layers[index], "Layer not found: " .. index) + + if type(index) == "string" then + for i, layer in ipairs(self.layers) do + if layer.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 +end + +function Map:update(dt) + for gid, tile in pairs( self.tiles ) do + local update + local t + + if tile.animation then + update = false + + tile.time = tile.time + dt * 1000 + while tile.time > tonumber(tile.animation[tile.frame].duration) do + 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 + update = true + end + if update == true and self.tileInstances[gid] ~= nil then + for _, j in pairs(self.tileInstances[gid]) do + t = self.tiles[tile.animation[tile.frame].tileid + self.tilesets[tile.tileset].firstgid] + j.batch:set( j.id, t.quad, j.x, j.y, 0 ) + end + end + end + end + + + for _, layer in ipairs(self.layers) do + layer:update(dt) + end +end + +function Map:draw(sx, sy) + local current_canvas = framework.getCanvas() + framework.setCanvas(self.canvas) + self.canvas:clear() + + for _, layer in ipairs(self.layers) do + if layer.visible and layer.opacity > 0 then + self:drawLayer(layer) + end + end + + framework.setCanvas(current_canvas) + + framework.push() + framework.origin() + framework.draw(self.canvas, 0, 0, 0, sx, sy) + framework.pop() +end + +function Map:drawLayer(layer) + framework.setColor(255, 255, 255, 255 * layer.opacity) + layer:draw() + framework.setColor(255, 255, 255, 255) +end + +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") + + local bw = layer.batches.width + local bh = layer.batches.height + local sx = math.ceil((self.drawRange.sx - layer.x / self.tilewidth - 1) / bw) + local sy = math.ceil((self.drawRange.sy - layer.y / self.tileheight - 1) / bh) + local ex = math.ceil((self.drawRange.ex - layer.x / self.tilewidth + 1) / bw) + local ey = math.ceil((self.drawRange.ey - layer.y / self.tileheight + 1) / bh) + local mx = math.ceil(self.width / bw) + local my = math.ceil(self.height / bh) + + for by=sy, ey do + for bx=sx, ex do + if bx >= 1 and bx <= mx and by >= 1 and by <= my then + for _, batches in pairs(layer.batches.data) do + local batch = batches[by] and batches[by][bx] + + if batch then + framework.draw(batch, math.floor(layer.x), math.floor(layer.y)) + end + end + end + end + end +end + +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.2 } + local shadow = { 0, 0, 0, 255 * layer.opacity } + local reset = { 255, 255, 255, 255 * layer.opacity } + + local function sortVertices(obj) + local vertices = {{},{}} + + for _, vertex in ipairs(obj) do + table.insert(vertices[1], vertex.x) + table.insert(vertices[1], vertex.y) + table.insert(vertices[2], vertex.x+1) + table.insert(vertices[2], vertex.y+1) + end + + return vertices + end + + local function drawShape(obj, shape) + local vertices = sortVertices(obj) + + if shape == "polyline" then + framework.setColor(shadow) + framework.line(vertices[2]) + framework.setColor(line) + framework.line(vertices[1]) + + return + elseif shape == "polygon" then + framework.setColor(fill) + if not framework.isConvex(vertices[1]) then + local triangles = framework.triangulate(vertices[1]) + for _, triangle in ipairs(triangles) do + framework.polygon("fill", triangle) + end + else + framework.polygon("fill", vertices[1]) + end + else + framework.setColor(fill) + framework.polygon("fill", vertices[1]) + end + + framework.setColor(shadow) + framework.polygon("line", vertices[2]) + framework.setColor(line) + framework.polygon("line", vertices[1]) + end + + for _, object in ipairs(layer.objects) do + if object.shape == "rectangle" 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") + end + end + + framework.setColor(reset) + for _, batch in pairs(layer.batches) do + framework.draw(batch, 0, 0) + end +end + +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 + framework.draw(layer.image, layer.x, layer.y) + end +end + +function Map:drawWorldCollision(collision) + for _, obj in ipairs(collision) do + framework.polygon("line", collision.body:getWorldPoints(obj.shape:getPoints())) + end +end + +function Map:resize(w, h) + self.canvas = framework:newCanvas(w, h) +end + +function Map:convertIsometricToScreen(x, y) + local mw = self.width + local tw, th = self.tilewidth, self.tileheight + local ox = mw * tw / 2 + + local sx = (x - y) + ox + local sy = (x + y) / 2 + + return sx, sy +end + +function Map:convertScreenToIsometric(x, y) + local mw, mh = self.width, self.height + local tw, th = self.tilewidth, self.tileheight + local ox = mw * tw / 2 + local oy = mh * th / 2 + + local tx = (x / 2 + y) - ox / 2 + local ty = (-x / 2 + y) + oy + + return tx, ty +end + +function Map:convertTileToScreen(x, y) + local tw, th = self.tilewidth, self.tileheight + + local sx = x * tw + local sy = y * th + + return sx, sy +end + +function Map:convertScreenToTile(x, y) + local tw, th = self.tilewidth, self.tileheight + + local tx = x / tw + local ty = y / th + + return tx, ty +end + +function Map:convertIsometricTileToScreen(x, y) + local mw = self.width + local tw, th = self.tilewidth, self.tileheight + local ox = mw * tw / 2 + + local sx = (x - y) * tw / 2 + ox + local sy = (x + y) * th / 2 + + return sx, sy +end + +function Map:convertScreenToIsometricTile(x, y) + local mw = self.width + local tw, th = self.tilewidth, self.tileheight + local ox = mw * tw / 2 + + local tx = y / th + (x - ox) / tw + local ty = y / th - (x - ox) / tw + + return tx, ty +end + +function Map:convertStaggeredTileToScreen(x, y) + local tw, th = self.tilewidth, self.tileheight + + local sx = x * tw + math.abs(math.ceil(y) % 2) * (tw / 2) - (math.ceil(y) % 2 * tw/2) + local sy = y * (th / 2) + th/2 + + return sx, sy +end + +function Map:convertScreenToStaggeredTile(x, y) + local function topLeft(x, y) + if (math.ceil(y) % 2) then + return x, y - 1 + else + return x - 1, y - 1 + end + end + + local function topRight(x, y) + if (math.ceil(y) % 2) then + return x + 1, y - 1 + else + return x, y - 1 + end + end + + local function bottomLeft(x, y) + if (math.ceil(y) % 2) then + return x, y + 1 + else + return x - 1, y + 1 + end + end + + local function bottomRight(x, y) + if (math.ceil(y) % 2) then + return x + 1, y + 1 + else + return x, y + 1 + end + end + + local tw, th = self.tilewidth, self.tileheight + local hh = th / 2 + local ratio = th / tw + + local tx = x / tw + local ty = y / th * 2 + + local ctx = math.ceil(x / tw) + local cty = math.ceil(y / th) * 2 + + local rx = x - ctx * tw + local ry = y - (cty / 2) * th + + if (hh - rx * ratio > ry) then + return topLeft(tx, ty) + elseif (-hh + rx * ratio > ry) then + return topRight(tx, ty) + elseif (hh + rx * ratio < ry) then + return bottomLeft(tx, ty) + elseif (hh * 3 - rx * ratio < ry) then + return bottomRight(tx, ty) + end + + return tx, ty +end + +return Map