From 4ec2003d731f95956d474dca470714bd2aa8e1fd Mon Sep 17 00:00:00 2001 From: Randy McShandy Date: Thu, 11 Dec 2025 22:38:13 -0600 Subject: [PATCH] Manually track pared-down STI --- main/assets.lua | 2 +- main/main.lua | 2 +- main/plugin/sti/CHANGELOG.md | 474 ++++++++ main/plugin/sti/LICENSE.md | 26 + main/plugin/sti/README.md | 96 ++ main/plugin/sti/atlas.lua | 159 +++ main/plugin/sti/graphics.lua | 132 +++ main/plugin/sti/init.lua | 1748 +++++++++++++++++++++++++++++ main/plugin/sti/plugins/box2d.lua | 323 ++++++ main/plugin/sti/plugins/bump.lua | 193 ++++ main/plugin/sti/utils.lua | 217 ++++ 11 files changed, 3370 insertions(+), 2 deletions(-) create mode 100644 main/plugin/sti/CHANGELOG.md create mode 100644 main/plugin/sti/LICENSE.md create mode 100644 main/plugin/sti/README.md create mode 100644 main/plugin/sti/atlas.lua create mode 100644 main/plugin/sti/graphics.lua create mode 100644 main/plugin/sti/init.lua create mode 100644 main/plugin/sti/plugins/box2d.lua create mode 100644 main/plugin/sti/plugins/bump.lua create mode 100644 main/plugin/sti/utils.lua diff --git a/main/assets.lua b/main/assets.lua index f92ffb1..8e287d0 100644 --- a/main/assets.lua +++ b/main/assets.lua @@ -1,7 +1,7 @@ -- assets.lua -- Asset storage -local sti = require('plugin/STI/sti') +local sti = require('plugin/sti') -- You need a cuneiform font for this one IntroMessage = "\n𒊩𒌆𒋼𒀯𒀭𒁶𒈨𒂗\n𒁓𒋧𒈠𒃲\n𒍣𒂵" diff --git a/main/main.lua b/main/main.lua index 5b68bf4..2d4b3af 100644 --- a/main/main.lua +++ b/main/main.lua @@ -1,6 +1,6 @@ -- main.lua -local sti = require('plugin/STI/sti') +local sti = require('plugin/sti') local conf = require('conf') local assets = require('assets') diff --git a/main/plugin/sti/CHANGELOG.md b/main/plugin/sti/CHANGELOG.md new file mode 100644 index 0000000..507afee --- /dev/null +++ b/main/plugin/sti/CHANGELOG.md @@ -0,0 +1,474 @@ +# Change Log + +## 2019-03-24: v1.2.3.0 + +* Added: support for changing layer tiles at runtime (thanks @IoriBranford) +* Added: object's name and type now exposed to bump world (thanks @npinochet) +* Added: support for LOVE 11.0 (thanks @tyraindreams, @Krankdud, @AndrewRublyov) +* Added: support for Box2D dynamic bodies via "dynamic" property (thanks @lemilonkh) +* Added: tutorial formerly hosted by lua.space to /tutorials +* Added: support for restitution, friction, and filter properties to Box2D plugin (thanks @DrJamgo) +* Added: support for infinite maps +* Added: support for layer groups (thanks @daxartio) +* Added: support for point objects +* Changed: loading a map now asserts (thanks @Alloyed) +* Fixed: error when translate coords are nil in Map.draw (thanks @camchenry) +* Fixed: create new tile instance list if one doesn't exist (thanks @IoriBranford) +* Fixed: error when translate coords are nil in box2d_draw (thanks @DanielPower) +* Fixed: incorrect Y offset when tile height is different than map's tileheight (thanks @IoriBranford) +* Fixed: tile objects now scale correctly (thanks @Jules5) +* Fixed: tile object transform issues (thanks @DavidJTFerguson37) +* Fixed: clobbering directory structure if several levels deep (thanks @meeces2911) +* Fixed: rotate_vertex was not compensating for height offset (thanks @Schwusch) + +## 2017-05-18: v0.18.2.1 + +* Changed: map.draw, map.box2d_draw, and map.bump_draw now take transform arguments. This (finally!) allows both scaling and translating to work together. +* Fixed: A bug where cached textures were not able to be loaded more than once (thanks @IoriBranford) + +## 2017-05-07: v0.18.2.0 + +* Added: dummy graphics shim for headless mode (thanks @matiasah) +* Changed: plugins now load using require() (thanks @benwebber) +* Fixed: typo causing staggered maps to crash (thanks @Drauthius) +* Fixed: a bug where scaling the map could cause graphical glitches +* Fixed: transparent colour code + +## 2017-02-01: v0.18.1.0 + +* Added: support for sorting objects +* Changed: box2d and bump plugins to be explicit when dealing with object groups within tiles +* Fixed: rendering staggered and hexogonal maps +* Removed: Map.drawRange was made obselete + +## 2017-02-01: v0.16.0.4 + +* Added: support for passing map table in directly (thanks @prust) +* Added: support for collision object groups in bump.lua plugin (thanks @DanielPower) +* Added: support for transparent colour mask (thanks @matiasah) +* Changed: batch size limit from 100 to default(1000) +* Changed: drawing layers now inherits colour settings (thanks @josefnpat) +* Fixed: a leaking global + +## 2016-08-22: v0.16.0.3 + +* Changed: box2d_draw plugin now uses correct shape (thanks @buckle2000) +* Fixed: a collision offset issue in bump_draw (thanks @rosshadden) +* Fixed: a bug where drawing multiple maps didn't always work properly + +## 2016-07-18: v0.16.0.2 + +* Added: busted tests! +* Changed: structure of git repo is now more user friendly +* Changed: converted map.lua into init.lua and changed isolation code +* Changed: STI.new() -> STI() +* Changed: Map:convertWorldToScreen -> Map:convertTileToPixel +* Changed: Map:convertScreenToWorld -> Map:convertPixelToTile +* Fixed: hexagonal maps now display in correct location (thanks @bjorn) + +## 2016-06-09: v0.16.0.1 + +* Changed: moved local functions to an internal utils file to tidy things up +* Changed: Box2D plugin now uses utils file instead of duplicate functions + +## 2016-06-09: v0.16.0.0 + +* Changed: Custom Properties now support proper boolean values in both Box2D and Bump plugins + +## 2016-06-09: v0.14.2.0 + +* Added: proper support for Layer offsets (thanks @Drauthius!) + +## 2016-05-19: v0.14.1.13 + +* Added: test maps for visual varification between Tiled and STI +* Added: Map:swapTile so you can cleanly swap tiles in Tile Layers +* Changed: all conversion functions consolidated into two functions (thanks @entrancejew) +* Fixed: shapes were off in noman's land in some cases (thanks @loribranford) + +## 2016-01-12: v0.14.1.12 + +* Added: basic support for object layers in Bump plugin (thanks @premek) +* Changed: new line token from CRLF to LF +* Fixed: sprite batches should now respect the map draw order + +## 2016-01-01: v0.14.1.11 + +* Fixed: various bugs in the Box2D plugin (thanks @ChrisWeisiger) +* Fixed: various bugs in the Bump plugin (thanks @bobbyjoness) + +## 2015-12-31: v0.14.1.10 + +* Fixed: Box2D plugin was not recognizing a tile's embedded object group + +## 2015-11-19: v0.14.1.9 + +* Changed: key in image cache to formatted path of image + +## 2015-11-16: v0.14.1.8 + +* Added: image cache to STI module [sponsored by Binary Cocoa] +* Added: STI:flush() to clear out image cache + +## 2015-11-15: v0.14.1.7 + +* Added: support for offsetting maps [sponsored by Binary Cocoa] +* Changed: Map.setDrawRange is more optimized via recycling tables +* Changed: render order now defaults to "right-down" + +## 2015-11-07: v0.14.1.6 + +* Fixed: tileset images not being properly filtered +* Fixed: bump.lua plugin missing world argument in draw + +## 2015-10-14: v0.14.1.5 + +* Added: bump.lua plugin (thanks @bobbyjoness) + +## 2015-10-12: v0.14.1.4 + +* Fixed: removing a layer now properly removes tile and object instances +* Fixed: box2d plugin now properly removes collision objects + +## 2015-10-09: v0.14.1.3 + +* Fixed: flipping animated tiles properly display +* Fixed: rotating animated tiles properly display +* Fixed: rotating tile objects properly display +* Fixed: box2d plugin properly creates rotated and flipped tile objects +* Fixed: box2d plugin no longer crashes when drawing a line with two vertices + +## 2015-10-07: v0.14.1.2 + +* Added: support for all render orders (rd, ru, ld, lu) +* Added: support for sensors in the box2d plugin (only works on individual tiles and objects; sensor = true) +* Changed: addCustomLayer's index argument is now optional and defaults to the end of the array +* Fixed: a crash when using Base64 (uncompressed) with LOVE 0.9.2 + +## 2015-10-03: v0.14.1.1 + +* Added: support for gzip compressed maps (requires LOVE 0.10.0+) + +## 2015-09-30: v0.14.1.0 + +* Added: support for Base64 compressed maps (requires LuaJIT) +* Added: support for zlib compressed maps (requires LOVE 0.10.0+) + +## 2015-09-28: v0.14.0.1 + +* Added: Support for all staggered types (x/y, even/odd, iso/hex) + +## 2015-09-27: v0.14.0.0 + +* Added: Hexagonal map support (thanks EntranceJew!) +* Added: Error message for compressed maps +* Fixed: box2d plugin threw an error in some cases (thanks maxha651!) + +## 2015-09-17: v0.13.1.4 + +* Changed: sanity checks now search for love.physics instead of love.physics.* + +## 2015-09-16: v0.13.1.3 + +* Changed: Improved documentation + +## 2015-09-16: v0.13.1.2 + +* Changed: Simplified plugins +* Changed: Namespaced the box2d plugin +* Removed: Non-LOVE frameworks (they didn't work) + +## 2015-09-16: v0.13.1.1 + +* Added: LDoc documentation +* Added: Plugin system where devs can extend STI +* Added: Reinstated the Box2D integration as a plugin + +## 2015-09-15: v0.13.1.0 + +* Added: Map:convertToCustomLayer() now returns the layer +* Changed: Tightened localization of some functions +* Removed: Box2D collision integration +* Removed: Unused functions + +## 2015-07-31: v0.12.3.0 + +* Added: Tiled version number to Map.tiledversion +* Added: Map.objects table indexed by unique object IDs +* Added: A better error message when trying to use Tile Collections +* Changed: Version number should now match Tiled's version number +* Changed: You must now add ".lua" in the filename of a new map as this is consistent with other libraries +* Changed: Renamed "pure" framework to "lua" (still doesn't work, though!) +* Changed: Map:setDrawRange no longer inverts tx and ty for you, do it yourself! +* Changed: Map:draw no longer accepts scale values, use love.graphics.scale! +* Fixed: A bug where tile objects were drawing an object border +* Removed: Corona framework file + +## 2015-03-22: v0.9.8 + +* Fixed: A bug where Tiles without a Properties list would crash + +## 2015-02-02: v0.9.7 + +* Added: userdata to Box2D fixtures +* Changed: changelog.txt -> CHANGELOG.md +* Changed: Flipping tiles now happens in both tile layers and object layers +* Fixed: A bug where tile objects were drawing oddly in some cases +* Fixed: A bug where circles would error if physics was disabled + +## 2015-01-28: v0.9.6 + +* Added: getLayerProperties(), getTileProperties(), and getObjectProperties() +* Fixed: A bug where flipped tiles crashed STI during initCollision() +* Fixed: Flipped collision tiles now have correct offset +* Removed: Reverted the change in v0.9.3 that filled in empty tiles with false + +## 2014-12-15: v0.9.5 + +* Fixed: A bug where tile collision objects were using the wrong size in some cases +* Fixed: A bug where flipped tiles weren't always creating collision objects + +## 2014-12-05: v0.9.4 + +* Changed: STI's canvas plays nicely with other libraries +* Changed: addCustomLayer() now returns a handle on the created layer + +## 2014-12-03: v0.9.3 + +* Added: Local Tile IDs to Tile objects +* Added: Terrain information +* Fixed: Some conversion functions +* Changed: Tile Layers now contain "false" instead of "nil" where there is no tile +* Changed: Added \_LICENSE, \_URL, \_VERSION, and \_DESCRIPTION properties to core STI object + +## 2014-09-29: v0.9.2 + +* Added: Support for drawing tiles in object layers +* Fixed: Incorrect calculation of some collision objects + +## 2014-09-26: v0.9.1 + +* Fixed: A crash when a collidable tile is initialized but not used +* Removed: Public access to formatPath(), rotateVertex(), and convertEllipseToPolygon() + +## 2014-09-24: v0.9.0 + +* Added: Animated tiles! (Thanks to Clean3d) +* Fixed: A crash when a collidable rectangle has no rotation value +* Fixed: Incorrect values given to orthogonal collision objects + +## 2014-09-24: v0.8.3 + +* Added: Map:convertScreenToTile() and Map:convertTileToScreen() +* Added: Map:convertScreenToIsometricTile() and Map:convertIsometricTileToScreen() +* Added: Map:convertScreenToStaggeredTile() and Map:convertStaggeredTileToScreen() +* Fixed: Map:removeLayer() now works properly +* Changed: Tile Objects now use the tile's collision map by default + +## 2014-09-22: v0.8.2 + +* Added: "collidable" property for objects, tiles, and layers + * if collidable is set to true in Tiled, STI will pick it up and set all appropriate entities to collidable objects +* Fixed: Physics module no long required if not needed. +* Fixed: Whitespace discrepencies +* Changed: Map:initWorldCollision() now supports a whole lot more + +## 2014-09-21: v0.8.1 + +* Added: README now lists minimum requirements +* Changed: README updated with new collision system +* Changed: Map:enableCollision() renamed to Map:initWorldCollision() +* Changed: Map:drawCollisionMap() renamed to Map:drawWorldCollision() +* Changed: Updated framework files (still no real Lua/Corona support) +* Changed: Tidied up collision code +* Removed: Map:getCollisionMap() + +## 2014-09-20: v0.8.0 + +* Added: Box2D collision via Map:enableCollision() +* Added: Map:convertEllipseToPolygon() + +## 2014-09-17: v0.7.6 + +* Added: Map:convertScreenToIsometric and Map:convertIsometricToScreen +* Added: Map:setObjectCoordinates +* Added: Map:rotateVertex +* Fixed: Adjusted map positioning for Isometric and Staggered maps +* Fixed: Object positioning in Isometric maps +* Removed: Temporary fix for Tiled 0.9.1 + +## 2014-08-05: v0.7.5 + +* Fixed: Properties offset by 1 +* Fixed: Drawing a single Layer can now use Layer's name/index + +## 2014-04-28: v0.7.4 + +* Fixed: Canvas resize type + +## 2014-04-18: v0.7.3 + +* Fixed: Canvas using wrong filter + +## 2014-04-08: v0.7.2 + +* Removed: Dependency for LuaJIT's bitwise operations + +## 2014-04-08: v0.7.1 + +* Added: Map:resize(w, h) +* Changed: Map:draw() now takes two optional arguments: ScaleX and ScaleY +* Changed: STI now draws to a Canvas before drawing to screen (fixes scaling oddities) + +## 2014-04-07: v0.7.0 + +* Added: Files for Corona and Pure Lua implementation +* Changed: Restructured sti.lua into several files +* Changed: Library is now LOVE agnostic and should allow for implementation of other frameworks + +## 2014-03-1 : v0.6.16 + +* Changed: Ellipses now use polygons instead of... Not polygons. + +## 2014-03-1 : v0.6.15 + +* Fixed: Tile spacing calculated properly in all cases + +## 2014-02-0 : v0.6.14 + +* Fixed: Tile properties ACTUALLY being added now! + +## 2014-01-2 : v0.6.13 + +* Added: Missing Tile Flag + +## 2014-01-2 : v0.6.12 + +* Added: drawCollisionMap() now supports Isometric and Staggered maps +* Changed: drawCollisionMap() now requires a collision map parameter +* Changed: setCollisionMap() renamed to getCollisionMap() +* Changed: getCollisionMap() now returns the collision map +* Fixed: Tile properties not being added +* Removed: Map.collision table removed + +## 2014-01-2 : v0.6.11 + +* Added: Descriptive error messages +* Fixed: Image filters for scaling + +## 2014-01-2 : v0.6.10 + +* Fixed: Optimized load time + +## 2014-01-25: v0.6.9 + +* Fixed: Parallax Scrolling + +## 2014-01-25: v0.6.8 + +* Changed: Revised and restructured code +* Changed: createCollisionMap() renamed to setCollisionMap() +* Changed: newCustomLayer() renamed to addCustomLayer() + +## 2014-01-24: v0.6.7 + +* Fixed: Number of tiles wasn't calculated properly + +## 2014-01-24: v0.6.6 + +* Fixed: Spacing wasn't calculated properly + +## 2014-01-24: v0.6.5 + +* Added: Staggered Maps + +## 2014-01-24: v0.6.4 + +* Added: Isometric Maps + +## 2014-01-20: v0.6.3 + +* Added: Tile Flags (flip/rotation) + +## 2014-01-20: v0.6.2 + +* Fixed: A scaling bug + +## 2014-01-19: v0.6.1 + +* Fixed: A bug causing the Collision Map to be nil + +## 2014-01-19: v0.6.0 + +* Added: Sprite Batches + +## 2014-01-19: v0.5.0 + +* Added: Draw Range optimization + +## 2014-01-18: v0.4.3 + +* Added: Layer draw offsets + +## 2014-01-17: v0.4.2 + +* Changed: Organized library a little better + +## 2014-01-17: v0.4.1 + +* Fixed: Tiles incorrectly offset +* Fixed: Drawing concave polygons + +## 2014-01-17: v0.4.0 + +* Added: Draw Object Layers + +## 2014-01-16: v0.3.3 + +* Added: Create new Custom Layer +* Added: Callbacks for all layers +* Added: Remove Layer +* Changed: Simplified sti.new() + +## 2014-01-16: v0.3.2 + +* Fixed: Crash if using Tiled 0.9.1 +* Changed: Map structure to remove "map" table + +## 2014-01-16: v0.3.1 + +* Added: Update callback to Custom Layers + +## 2014-01-16: v0.3.0 + +* Added: Support for converting layers to Custom Layers +* Changed: sti.new() no longer requires the file extension + +## 2014-01-15: v0.2.2 + +* Added: Support for basic collision layer + +## 2014-01-15: v0.2.1 + +* Added: Support for map instances +* Added: Name alias to layer indices +* Changed: Sandboxed map environment +* Changed: Data structures are more efficient +* Removed: Unnecessary update function + +Thanks to JarrettBillingsley for many of these changes + +## 2014-01-14: v0.2.0 + +* Fixed: Drawing Tile Offset +* Changed: Tile Layer data structure is more efficient +* Changed: Simplified Quad generation + +## 2014-01-14: v0.1.0 + +* Initial Commit +* Added: Orthogonal Map support +* Added: Draw Tile Layers +* Added: Draw Image Layers +* Added: Ignore Hidden Layers +* Added: Layer Opacity diff --git a/main/plugin/sti/LICENSE.md b/main/plugin/sti/LICENSE.md new file mode 100644 index 0000000..33c6729 --- /dev/null +++ b/main/plugin/sti/LICENSE.md @@ -0,0 +1,26 @@ +# Simple Tiled Implementation + +This code is licensed under the [**MIT/X11 Open Source License**][MIT]. + +Copyright (c) 2019 Landon Manning - LManning17@gmail.com - [LandonManning.com][LM] + +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. + +[MIT]: https://www.opensource.org/licenses/mit-license.html +[LM]: https://LandonManning.com diff --git a/main/plugin/sti/README.md b/main/plugin/sti/README.md new file mode 100644 index 0000000..c1be91c --- /dev/null +++ b/main/plugin/sti/README.md @@ -0,0 +1,96 @@ +# Simple Tiled Implementation + +[![Join the chat at https://gitter.im/karai17/Simple-Tiled-Implementation](https://badges.gitter.im/karai17/Simple-Tiled-Implementation.svg)](https://gitter.im/karai17/Simple-Tiled-Implementation?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +If you like STI, consider tossing me a few monies via [**PayPal**][paypal]. + +Simple Tiled Implementation is a [**Tiled**][Tiled] map loader and renderer designed for the *awesome* [**LÖVE**][LOVE] framework. Please read the [**documentation**][dox] to learn how it works, or check out the tutorials included in this repo. + +## Quick Example + +```lua +-- This example uses the included Box2D (love.physics) plugin!! + +local sti = require "sti" + +function love.load() + -- Grab window size + windowWidth = love.graphics.getWidth() + windowHeight = love.graphics.getHeight() + + -- Set world meter size (in pixels) + love.physics.setMeter(32) + + -- Load a map exported to Lua from Tiled + map = sti("assets/maps/map01.lua", { "box2d" }) + + -- Prepare physics world with horizontal and vertical gravity + world = love.physics.newWorld(0, 0) + + -- Prepare collision objects + map:box2d_init(world) + + -- Create a Custom Layer + map:addCustomLayer("Sprite Layer", 3) + + -- Add data to Custom Layer + local spriteLayer = map.layers["Sprite Layer"] + spriteLayer.sprites = { + player = { + image = love.graphics.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 + love.graphics.draw(sprite.image, x, y, r) + end + end +end + +function love.update(dt) + map:update(dt) +end + +function love.draw() + -- Draw the map and all objects within + love.graphics.setColor(1, 1, 1) + map:draw() + + -- Draw Collision Map (useful for debugging) + love.graphics.setColor(1, 0, 0) + map:box2d_draw() + + -- Please note that map:draw, map:box2d_draw, and map:bump_draw take + -- translate and scale arguments (tx, ty, sx, sy) for when you want to + -- grow, shrink, or reposition your map on screen. +end +``` + +## Requirements + +This library recommends LÖVE 11.x and Tiled 1.2.x. If you are updating from an older version of Tiled, please re-export your Lua map files. + +## License + +This code is licensed under the [**MIT/X11 Open Source License**][MIT]. Check out the LICENSE file for more information. + +[Tiled]: http://www.mapeditor.org/ +[LOVE]: https://www.love2d.org/ +[dox]: http://karai17.github.io/Simple-Tiled-Implementation/ +[MIT]: http://www.opensource.org/licenses/mit-license.html +[paypal]: https://www.paypal.me/LandonManning diff --git a/main/plugin/sti/atlas.lua b/main/plugin/sti/atlas.lua new file mode 100644 index 0000000..302b332 --- /dev/null +++ b/main/plugin/sti/atlas.lua @@ -0,0 +1,159 @@ +---- Texture atlas complement for the Simple Tiled Implementation +-- @copyright 2022 +-- @author Eduardo Hernández coz.eduardo.hernandez@gmail.com +-- @license MIT/X11 + +local module = {} + +--- Create a texture atlas +-- @param files Array with filenames +-- @param sort If "size" will sort by size, or if "id" will sort by id +-- @param ids Array with ids of each file +-- @param pow2 If true, will force a power of 2 size +function module.Atlas( files, sort, ids, pow2 ) + + local function Node(x, y, w, h) + return {x = x, y = y, w = w, h = h} + end + + local function nextpow2( n ) + local res = 1 + while res <= n do + res = res * 2 + end + return res + end + + local function loadImgs() + local images = {} + for i = 1, #files do + images[i] = {} + --images[i].name = files[i] + if ids then images[i].id = ids[i] end + images[i].img = love.graphics.newImage( files[i] ) + images[i].w = images[i].img:getWidth() + images[i].h = images[i].img:getHeight() + images[i].area = images[i].w * images[i].h + end + if sort == "size" or sort == "id" then + table.sort( images, function( a, b ) return ( a.area > b.area ) end ) + end + return images + end + + --TODO: understand this func + local function add(root, id, w, h) + if root.left or root.right then + if root.left then + local node = add(root.left, id, w, h) + if node then return node end + end + if root.right then + local node = add(root.right, id, w, h) + if node then return node end + end + return nil + end + + if w > root.w or h > root.h then return nil end + + local _w, _h = root.w - w, root.h - h + + if _w <= _h then + root.left = Node(root.x + w, root.y, _w, h) + root.right = Node(root.x, root.y + h, root.w, _h) + else + root.left = Node(root.x, root.y + h, w, _h) + root.right = Node(root.x + w, root.y, _w, root.h) + end + + root.w = w + root.h = h + root.id = id + + return root + end + + local function unmap(root) + if not root then return {} end + + local tree = {} + if root.id then + tree[root.id] = {} + tree[root.id].x, tree[root.id].y = root.x, root.y + end + + local left = unmap(root.left) + local right = unmap(root.right) + + for k, v in pairs(left) do + tree[k] = {} + tree[k].x, tree[k].y = v.x, v.y + end + for k, v in pairs(right) do + tree[k] = {} + tree[k].x, tree[k].y = v.x, v.y + end + + return tree + end + + local function bake() + local images = loadImgs() + + local root = {} + local w, h = images[1].w, images[1].h + + if pow2 then + if w % 1 == 0 then w = nextpow2(w) end + if h % 1 == 0 then h = nextpow2(h) end + end + + repeat + local node + + root = Node(0, 0, w, h) + + for i = 1, #images do + node = add(root, i, images[i].w, images[i].h) + if not node then break end + end + + if not node then + if h <= w then + if pow2 then h = h * 2 else h = h + 1 end + else + if pow2 then w = w * 2 else w = w + 1 end + end + else + break + end + until false + + local limits = love.graphics.getSystemLimits() + if w > limits.texturesize or h > limits.texturesize then + return "Resulting texture is too large for this system" + end + + local coords = unmap(root) + local map = love.graphics.newCanvas(w, h) + love.graphics.setCanvas( map ) +-- love.graphics.clear() + + for i = 1, #images do + love.graphics.draw(images[i].img, coords[i].x, coords[i].y) + if ids then coords[i].id = images[i].id end + end + love.graphics.setCanvas() + + if sort == "ids" then + table.sort( coords, function( a, b ) return ( a.id < b.id ) end ) + end + + return { image = map, coords = coords } + end + + return bake() +end + +return module diff --git a/main/plugin/sti/graphics.lua b/main/plugin/sti/graphics.lua new file mode 100644 index 0000000..6acf8d6 --- /dev/null +++ b/main/plugin/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/main/plugin/sti/init.lua b/main/plugin/sti/init.lua new file mode 100644 index 0000000..646a224 --- /dev/null +++ b/main/plugin/sti/init.lua @@ -0,0 +1,1748 @@ +--- 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 atlas = require(cwd .. "atlas") +local Map = {} +Map.__index = Map + +local function new(map, plugins, ox, oy) + local dir = "" + + if type(map) == "table" then + map = setmetatable(map, Map) + else + -- Check for valid map type + local ext = map:sub(-4, -1) + assert(ext == ".lua", string.format( + "Invalid file type: %s. File must be of type: lua.", + ext + )) + + -- Get directory of map + dir = map:reverse():find("[/\\]") or "" + if dir ~= "" then + dir = map:sub(1, 1 + (#map - dir)) + end + + -- Load map + map = setmetatable(assert(love.filesystem.load(map))(), Map) + end + + map:init(dir, plugins, ox, oy) + + return map +end + +--- Instance a new map. +-- @param map Path to the map file or the map table itself +-- @param plugins A list of plugins to load +-- @param ox Offset of map on the X axis (in pixels) +-- @param oy Offset of map on the Y axis (in pixels) +-- @return table The loaded Map +function STI.__call(_, map, plugins, ox, oy) + return new(map, plugins, ox, oy) +end + +--- Flush image cache. +function STI:flush() + self.cache = {} +end + +--- Map object + +--- Instance a new map +-- @param path Path to the map file +-- @param plugins A list of plugins to load +-- @param ox Offset of map on the X axis (in pixels) +-- @param oy Offset of map on the Y axis (in pixels) +function Map:init(path, plugins, ox, oy) + if type(plugins) == "table" then + self:loadPlugins(plugins) + end + + self:resize() + self.objects = {} + self.tiles = {} + self.tileInstances = {} + self.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(not tileset.filename, "STI does not support external Tilesets.\nYou need to embed all Tilesets.") + + if tileset.image then + -- 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) + elseif tileset.tilecount > 0 then + -- Build atlas for image collection + local files, ids = {}, {} + for j = 1, #tileset.tiles do + files[ j ] = utils.format_path(path .. tileset.tiles[j].image) + ids[ j ] = tileset.tiles[j].id + end + + local map = atlas.Atlas( files, "ids", ids ) + + if lg.isCreated then + local formatted_path = utils.format_path(path .. tileset.name) + + if not STI.cache[formatted_path] then + -- No need to fix transparency color for collections + utils.cache_image(STI, formatted_path, map.image) + tileset.image = map.image + else + tileset.image = STI.cache[formatted_path] + end + end + + gid = self:setAtlasTiles(i, tileset, map.coords, gid) + end + 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 based on a single tileset image +-- @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 Tiles based on a texture atlas +-- @param index Index of the Tileset +-- @param tileset Tileset data +-- @param coords Tile XY location in the atlas +-- @param gid First Global ID in Tileset +-- @return number Next Tileset's first Global ID +function Map:setAtlasTiles(index, tileset, coords, gid) + local quad = lg.newQuad + local imageW = tileset.image:getWidth() + local imageH = tileset.image:getHeight() + + local firstgid = tileset.firstgid + for i = 1, #tileset.tiles do + local tile = tileset.tiles[i] + if tile.terrain then + terrain = {} + + for j = 1, #tile.terrain do + terrain[j] = tileset.terrains[tile.terrain[j] + 1] + end + end + + local tile = { + id = tile.id, + gid = firstgid + tile.id, + tileset = index, + class = tile.class, + quad = quad( + coords[i].x, coords[i].y, + tile.width, tile.height, + imageW, imageH + ), + properties = tile.properties or {}, + terrain = terrain, + animation = tile.animation, + objectGroup = tile.objectGroup, + frame = 1, + time = 0, + width = tile.width, + height = tile.height, + sx = 1, + sy = 1, + r = 0, + offset = tileset.tileoffset, + } + + -- Be aware that in collections self.tiles can be a sparse array + self.tiles[tile.gid] = tile + end + + return gid + #tileset.tiles +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 + tileX = (x - 1) * tileW + tile.offset.x + tileY = (y - 0) * tileH + tile.offset.y - tile.height + 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() + + --[[ + This snippet comes from 'monolifed' on the Love2D forums, + however it was more or less exactly the same code I was already writing + to implement the same parallax scrolling. I found his before + testing and polishing mine + https://love2d.org/forums/viewtopic.php?p=238378#p238378 + + previous code commented below the new. + + ]] + + tx, ty = tx or 0, ty or 0 + + for _, layer in ipairs(self.layers) do + if layer.visible and layer.opacity > 0 then + local px, py = layer.parallaxx or 1, layer.parallaxy or 1 + px, py = math.floor(tx * px), math.floor(ty * py) + lg.translate(px, py) + self:drawLayer(layer) + lg.translate(-px, -py) + end + end + + --[[ + lg.translate(math.floor(tx or 0), math.floor(ty or 0)) + + for _, layer in ipairs(self.layers) do + if layer.visible and layer.opacity > 0 then + self:drawLayer(layer) + end + end + ]] + + lg.pop() + + -- Draw canvas at 0,0; this fixes scissoring issues + -- Map is scaled to correct scale so the right section is shown + lg.push() + lg.origin() + lg.scale(sx or 1, sy or sx or 1) + + lg.setCanvas(current_canvas) + lg.draw(self.canvas) + + lg.pop() +end + +--- Draw an individual Layer +-- @param layer The Layer to draw +function Map.drawLayer(_, layer) + local r,g,b,a = lg.getColor() + -- if the layer has a tintcolor set + if layer.tintcolor then + r, g, b, a = unpack(layer.tintcolor) + a = a or 255 -- alpha may not be specified + lg.setColor(r/255, g/255, b/255, a/255) -- Tiled uses 0-255 + -- if a tintcolor is not given just use the current color + else + lg.setColor(r, g, b, a * layer.opacity) + end + 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) + -- we need pixel sizes for drawing + local imagewidth, imageheight = layer.image:getDimensions() + -- if we're repeating on the Y axis... + if layer.repeaty then + local x = imagewidth + local y = imageheight + while y < self.height * self.tileheight do + lg.draw(layer.image, x, y) + -- if we are *also* repeating on X + if layer.repeatx then + x = x + imagewidth + while x < self.width * self.tilewidth do + lg.draw(layer.image, x, y) + x = x + imagewidth + end + end + y = y + imageheight + end + -- if we're repeating on X alone... + elseif layer.repeatx then + local x = imagewidth + while x < self.width * self.tilewidth do + lg.draw(layer.image, x, layer.y) + x = x + imagewidth + end + end + 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/main/plugin/sti/plugins/box2d.lua b/main/plugin/sti/plugins/box2d.lua new file mode 100644 index 0000000..c6d4148 --- /dev/null +++ b/main/plugin/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/main/plugin/sti/plugins/bump.lua b/main/plugin/sti/plugins/bump.lua new file mode 100644 index 0000000..1d4b828 --- /dev/null +++ b/main/plugin/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/main/plugin/sti/utils.lua b/main/plugin/sti/utils.lua new file mode 100644 index 0000000..95e857a --- /dev/null +++ b/main/plugin/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 -- 2.49.0