├── .gitignore ├── preview.gif ├── assets ├── cat.ase ├── cat.png ├── gun.png ├── pig.ase ├── pig.png ├── yay.wav ├── font.ttf ├── icon.icns ├── icon.ico ├── icon.png ├── meow.ogg ├── music.ogg ├── oink.ogg ├── step.wav ├── thud.wav ├── tiles.png ├── bullet.png ├── cannon.wav ├── catjump.wav ├── music.sunvox ├── spawner.png ├── explosion.ase ├── explosion.png ├── signbubble.wav └── catandcannon.png ├── Makefile ├── src ├── systems │ ├── UpdateSystem.lua │ ├── FadeSystem.lua │ ├── HudSystem.lua │ ├── LifetimeSystem.lua │ ├── DrawBackgroundSystem.lua │ ├── AISystem.lua │ ├── WaveSystem.lua │ ├── TileMapRenderSystem.lua │ ├── CameraTrackingSystem.lua │ ├── SpawnSystem.lua │ ├── SpriteSystem.lua │ ├── PlatformingSystem.lua │ ├── PlayerControlSystem.lua │ └── BumpPhysicsSystem.lua ├── entities │ ├── TimerEvent.lua │ ├── Explosion.lua │ ├── Spawner.lua │ ├── MainHud.lua │ ├── TransitionScreen.lua │ ├── ScreenSplash.lua │ ├── Bullet.lua │ ├── Pig.lua │ └── Player.lua ├── assets.lua └── states │ ├── Intro.lua │ └── Level.lua ├── README.md ├── conf.lua ├── lib ├── sti │ ├── graphics.lua │ ├── utils.lua │ ├── plugins │ │ ├── bump.lua │ │ └── box2d.lua │ └── init.lua ├── gamestate.lua ├── beholder.lua ├── 30log.lua ├── gamera.lua ├── anim8.lua ├── bump.lua └── tiny.lua └── main.lua /.gitignore: -------------------------------------------------------------------------------- 1 | releases/ -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/preview.gif -------------------------------------------------------------------------------- /assets/cat.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/cat.ase -------------------------------------------------------------------------------- /assets/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/cat.png -------------------------------------------------------------------------------- /assets/gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/gun.png -------------------------------------------------------------------------------- /assets/pig.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/pig.ase -------------------------------------------------------------------------------- /assets/pig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/pig.png -------------------------------------------------------------------------------- /assets/yay.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/yay.wav -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for a love project 2 | 3 | run: 4 | love . 5 | 6 | .PHONY: run 7 | -------------------------------------------------------------------------------- /assets/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/font.ttf -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/meow.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/meow.ogg -------------------------------------------------------------------------------- /assets/music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/music.ogg -------------------------------------------------------------------------------- /assets/oink.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/oink.ogg -------------------------------------------------------------------------------- /assets/step.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/step.wav -------------------------------------------------------------------------------- /assets/thud.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/thud.wav -------------------------------------------------------------------------------- /assets/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/tiles.png -------------------------------------------------------------------------------- /assets/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/bullet.png -------------------------------------------------------------------------------- /assets/cannon.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/cannon.wav -------------------------------------------------------------------------------- /assets/catjump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/catjump.wav -------------------------------------------------------------------------------- /assets/music.sunvox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/music.sunvox -------------------------------------------------------------------------------- /assets/spawner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/spawner.png -------------------------------------------------------------------------------- /assets/explosion.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/explosion.ase -------------------------------------------------------------------------------- /assets/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/explosion.png -------------------------------------------------------------------------------- /assets/signbubble.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/signbubble.wav -------------------------------------------------------------------------------- /assets/catandcannon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakpakin/CommandoKibbles/HEAD/assets/catandcannon.png -------------------------------------------------------------------------------- /src/systems/UpdateSystem.lua: -------------------------------------------------------------------------------- 1 | local UpdateSystem = tiny.processingSystem(class "UpdateSystem") 2 | 3 | UpdateSystem.filter = tiny.requireAll("update") 4 | 5 | function UpdateSystem:process(e, dt) 6 | e:update(dt) 7 | end 8 | 9 | return UpdateSystem 10 | -------------------------------------------------------------------------------- /src/entities/TimerEvent.lua: -------------------------------------------------------------------------------- 1 | local TimerEvent = class "TimerEvent" 2 | 3 | function TimerEvent:init(time, fn) 4 | self.lifetime = time 5 | self.timerCallback = fn 6 | end 7 | 8 | function TimerEvent:onLifeover() 9 | self.timerCallback() 10 | end 11 | 12 | return TimerEvent 13 | -------------------------------------------------------------------------------- /src/systems/FadeSystem.lua: -------------------------------------------------------------------------------- 1 | local FadeSystem = tiny.processingSystem(class "FadeSystem") 2 | 3 | FadeSystem.filter = tiny.requireAll("fadeTime", "alpha") 4 | 5 | function FadeSystem:process(e, dt) 6 | e.alpha = math.min(1, math.max(0, e.alpha - dt / e.fadeTime)) 7 | end 8 | 9 | return FadeSystem 10 | -------------------------------------------------------------------------------- /src/systems/HudSystem.lua: -------------------------------------------------------------------------------- 1 | local HudSystem = tiny.processingSystem(class "HudSystem") 2 | HudSystem.isDrawSystem = true 3 | 4 | function HudSystem:init(layerFlag) 5 | self.filter = tiny.requireAll("drawHud", layerFlag) 6 | end 7 | 8 | function HudSystem:process(e, dt) 9 | e:drawHud(dt) 10 | end 11 | 12 | return HudSystem 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](https://github.com/bakpakin/CommandoKibbles/raw/master/preview.gif) 2 | 3 | ## Commando Kibbles 4 | 5 | Fight against invading pigs as a feisty feline with a cannon strapped to your back. Made for 6 | Ludum Dare 32, and updated to have cleaner code. 7 | 8 | # Running 9 | 10 | Uses [LOVE](https://love2d.org/) to run. Run the love executable from the project directory to test. 11 | Requires LOVE 11.1. 12 | -------------------------------------------------------------------------------- /src/systems/LifetimeSystem.lua: -------------------------------------------------------------------------------- 1 | local LifetimeSystem = tiny.processingSystem(class "LifetimeSystem") 2 | 3 | LifetimeSystem.filter = tiny.requireAll("lifetime") 4 | 5 | function LifetimeSystem:process(e, dt) 6 | e.lifetime = e.lifetime - dt 7 | if e.lifetime <= 0 then 8 | if e.onLifeover then 9 | e:onLifeover() 10 | end 11 | world:remove(e) 12 | end 13 | end 14 | 15 | return LifetimeSystem 16 | -------------------------------------------------------------------------------- /src/entities/Explosion.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local anim8 = require "lib.anim8" 3 | 4 | local Explosion = class "Explosion" 5 | 6 | Explosion.sprite = assets.img_explosion 7 | 8 | function Explosion:init(x, y) 9 | self.pos = {x = x, y = y} 10 | self.bg = true 11 | local g = anim8.newGrid(64, 64, assets.img_explosion:getWidth(), assets.img_explosion:getHeight()) 12 | self.animation = anim8.newAnimation(g('1-10', 1), 0.05) 13 | self.lifetime = 9 * 0.05 14 | end 15 | 16 | return Explosion 17 | -------------------------------------------------------------------------------- /src/systems/DrawBackgroundSystem.lua: -------------------------------------------------------------------------------- 1 | local DrawBackgroundSystem = tiny.system(class "DrawBackgroundSystem") 2 | DrawBackgroundSystem.isDrawSystem = true 3 | 4 | function DrawBackgroundSystem:init(r, g, b) 5 | self.r, self.g, self.b = r, g, b 6 | end 7 | 8 | function DrawBackgroundSystem:update(dt) 9 | local r1, g1, b1, a = love.graphics.getColor() 10 | love.graphics.setColor(self.r, self.g, self.b, 1) 11 | love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), love.graphics.getHeight()) 12 | love.graphics.setColor(r1, g1, b1, a) 13 | end 14 | 15 | return DrawBackgroundSystem 16 | -------------------------------------------------------------------------------- /src/systems/AISystem.lua: -------------------------------------------------------------------------------- 1 | local AISystem = tiny.processingSystem(class "AISystem") 2 | 3 | function AISystem:init(target) 4 | self.target = target 5 | end 6 | 7 | AISystem.filter = tiny.requireAll("ai", "pos", "platforming") 8 | 9 | function AISystem:process(e, dt) 10 | if not self.target then 11 | return 12 | end 13 | local targetx = self.target.pos.x 14 | local pos = e.pos 15 | local p = e.platforming 16 | p.moving = self.target.isAlive 17 | if targetx > pos.x then 18 | p.direction = 'r' 19 | end 20 | if targetx < pos.x then 21 | p.direction = 'l' 22 | end 23 | p.jumping = math.random() < 0.5 * dt 24 | end 25 | 26 | return AISystem 27 | -------------------------------------------------------------------------------- /src/entities/Spawner.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local Pig = require "src.entities.Pig" 3 | 4 | local Spawner = class "Spawner" 5 | 6 | Spawner.sprite = assets.img_spawner 7 | 8 | function Spawner:init(args) 9 | self.pos = {x = args.x + 32, y = args.y + 32} 10 | self.offset = {x = 32, y = 32} 11 | self.scale = {x = 1, y = 1} 12 | self.rot = 0 13 | self.bg = true 14 | self.isSpawner = true 15 | end 16 | 17 | function Spawner:update(dt) 18 | self.rot = self.rot + 2 * dt 19 | local s = math.random() * 0.2 + 0.9 20 | self.scale.x, self.scale.y = s, s 21 | end 22 | 23 | function Spawner:spawn() 24 | world:add(Pig(self.pos.x - 15, self.pos.y - 10, nil)) 25 | end 26 | 27 | return Spawner 28 | -------------------------------------------------------------------------------- /src/systems/WaveSystem.lua: -------------------------------------------------------------------------------- 1 | local TimerEvent = require "src.entities.TimerEvent" 2 | 3 | local WaveSystem = tiny.system(class "WaveSystem") 4 | 5 | WaveSystem.filter = tiny.requireAll("isEnemy") 6 | 7 | function WaveSystem:init(levelState) 8 | self.levelState = levelState 9 | end 10 | 11 | function WaveSystem:onAdd(e) 12 | self.levelState.enemiesSpawned = self.levelState.enemiesSpawned + 1 13 | end 14 | 15 | function WaveSystem:onRemove(e) 16 | local levelState = self.levelState 17 | levelState.enemiesKilled = levelState.enemiesKilled + 1 18 | if levelState.enemiesKilled >= levelState.totalEnemiesToKill then 19 | world:add(TimerEvent(1, function() 20 | levelState:nextWave() 21 | end)) 22 | end 23 | end 24 | 25 | return WaveSystem 26 | -------------------------------------------------------------------------------- /src/entities/MainHud.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | 3 | local MainHud = class "MainHud" 4 | 5 | function MainHud:drawHud(dt) 6 | local n = self.levelState.totalEnemiesToKill - self.levelState.enemiesKilled 7 | local d = self.levelState.totalEnemiesToKill 8 | love.graphics.setFont(assets.fnt_hud) 9 | love.graphics.printf("Wave " .. self.levelState.wave, 20, 20, 300, "left") 10 | love.graphics.setFont(assets.fnt_smallhud) 11 | love.graphics.printf(n .. "/" .. d .. " Pigs Remaining", 20, 60, 500, "left") 12 | love.graphics.printf("Total Pigs Killed: " .. self.levelState.score, love.graphics.getWidth() - 420, 20, 400, "right") 13 | end 14 | 15 | function MainHud:init(levelState) 16 | self.levelState = levelState 17 | self.hudBg = true 18 | end 19 | 20 | return MainHud 21 | -------------------------------------------------------------------------------- /src/systems/TileMapRenderSystem.lua: -------------------------------------------------------------------------------- 1 | local beholder = require 'lib.beholder' 2 | local TileMapRenderSystem = tiny.system(class "TileMapRenderSystem") 3 | TileMapRenderSystem.isDrawSystem = true 4 | 5 | local lgw, lgh, stale 6 | 7 | beholder.observe('resize', function(w, h) 8 | stale = true 9 | lgw = w 10 | lgh = h 11 | end) 12 | 13 | function TileMapRenderSystem:init(camera, tileMap) 14 | self.camera = camera 15 | self.tileMap = tileMap 16 | end 17 | 18 | function TileMapRenderSystem:update(dt) 19 | local c = self.camera 20 | local tm = self.tileMap 21 | local s = c:getScale() 22 | local tx, ty = c:getVisibleCorners() 23 | if stale then 24 | stale = nil 25 | tm:resize(lgw, lgh) 26 | end 27 | tm:update(dt) 28 | tm:draw(-tx + 16, -ty + 16, s) 29 | end 30 | 31 | return TileMapRenderSystem 32 | -------------------------------------------------------------------------------- /src/entities/TransitionScreen.lua: -------------------------------------------------------------------------------- 1 | local gamestate = require "lib.gamestate" 2 | local TransitionScreen = class "TransitionScreen" 3 | 4 | TransitionScreen.hudFg = true 5 | 6 | -- mode is true for "toblack" or false for "totransparent" 7 | function TransitionScreen:init(mode, newState) 8 | self.lifetime = 0.5 9 | self.newState = newState 10 | if mode then 11 | self.alpha = 0 12 | self.fadeTime = -0.5 13 | else 14 | self.alpha = 1 15 | self.fadeTime = 0.5 16 | end 17 | end 18 | 19 | function TransitionScreen:drawHud(dt) 20 | local r1, g1, b1, a = love.graphics.getColor() 21 | love.graphics.setColor(0, 0, 0, self.alpha) 22 | love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), love.graphics.getHeight()) 23 | love.graphics.setColor(r1, g1, b1, a) 24 | end 25 | 26 | function TransitionScreen:onLifeover() 27 | if self.newState then 28 | gamestate.switch(self.newState) 29 | end 30 | end 31 | 32 | return TransitionScreen 33 | -------------------------------------------------------------------------------- /src/systems/CameraTrackingSystem.lua: -------------------------------------------------------------------------------- 1 | local CameraTrackingSystem = tiny.processingSystem(class "CameraTrackingSystem") 2 | 3 | CameraTrackingSystem.filter = tiny.requireAll("cameraTrack", "pos") 4 | 5 | function CameraTrackingSystem:init(camera) 6 | self.camera = camera 7 | end 8 | 9 | local function round(x) 10 | return math.floor(x + 0.5) 11 | end 12 | 13 | function CameraTrackingSystem:process(e, dt) 14 | local xo, yo = e.cameraTrack.xoffset, e.cameraTrack.yoffset 15 | local x, y = e.pos.x + xo, e.pos.y + yo 16 | local xp, yp = self.camera:getPosition() 17 | local lerp = 0.1 18 | self.camera:setPosition(round(xp + (x - xp) * lerp), round(yp + (y - yp) * lerp)) 19 | end 20 | 21 | function CameraTrackingSystem:onAdd(e) 22 | local xo, yo = e.cameraTrack.xoffset, e.cameraTrack.yoffset 23 | local x, y = e.pos.x + xo, e.pos.y + yo 24 | self.camera:setPosition(round(x), round(y)) 25 | end 26 | 27 | return CameraTrackingSystem 28 | -------------------------------------------------------------------------------- /src/entities/ScreenSplash.lua: -------------------------------------------------------------------------------- 1 | local ScreenSplash = class("Screen Splash") 2 | local assets = require "src.assets" 3 | 4 | function ScreenSplash:drawHud() 5 | love.graphics.setFont(self.splash.fnt or assets.fnt_hud) 6 | local align = self.align 7 | local w, h = love.graphics.getWidth() * self.pos.x, love.graphics.getHeight() * self.pos.y 8 | local dx, dy = self.offset.x, self.offset.y 9 | if align == "center" then 10 | dx = dx - self.splash.width / 2 11 | end 12 | if align == "right" then 13 | dx = dx - self.splash.width 14 | end 15 | love.graphics.printf(self.splash.text, w + dx, h + dy, self.splash.width, self.align) 16 | end 17 | 18 | function ScreenSplash:init(x, y, text, width, fnt, align, xo, yo) 19 | self.pos = {x = x, y = y} 20 | self.offset = {x = xo or 0, y = yo or 0} 21 | self.hudBg = true 22 | self.align = align or "center" 23 | self.splash = { 24 | text = text, 25 | fnt = fnt, 26 | width = width or 400 27 | } 28 | end 29 | 30 | return ScreenSplash 31 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = nil 3 | t.version = "11.1" 4 | t.console = false 5 | t.window.title = "Commando Kibbles" 6 | t.window.icon = nil 7 | t.window.width = 900 8 | t.window.height = 600 9 | t.window.borderless = false 10 | t.window.resizable = true 11 | t.window.minwidth = 600 12 | t.window.minheight = 400 13 | t.window.fullscreen = false 14 | t.window.vsync = true 15 | t.window.fsaa = 0 16 | t.window.display = 1 17 | t.window.highdpi = false 18 | t.window.srgb = false 19 | t.window.x = nil 20 | t.window.y = nil 21 | t.modules.audio = true 22 | t.modules.event = true 23 | t.modules.graphics = true 24 | t.modules.image = true 25 | t.modules.joystick = false 26 | t.modules.keyboard = true 27 | t.modules.math = true 28 | t.modules.mouse = true 29 | t.modules.physics = false 30 | t.modules.sound = true 31 | t.modules.system = true 32 | t.modules.timer = true 33 | t.modules.window = true 34 | end 35 | -------------------------------------------------------------------------------- /src/systems/SpawnSystem.lua: -------------------------------------------------------------------------------- 1 | local TimerEvent = require "src.entities.TimerEvent" 2 | 3 | local SpawnSystem = tiny.system(class "SpawnSystem") 4 | 5 | SpawnSystem.filter = tiny.requireAll("isSpawner") 6 | 7 | function SpawnSystem:init(levelState) 8 | self.levelState = levelState 9 | self.time = 0 10 | end 11 | 12 | function SpawnSystem:update(dt) 13 | self.time = self.time + dt 14 | local levelState = self.levelState 15 | if levelState.isSpawning and levelState.enemiesSpawned < levelState.totalEnemiesToKill and self.time >= levelState.spawnInterval then 16 | local choice = math.ceil(math.random() * levelState.spawnerCount) 17 | for spnr in pairs(levelState.spawners) do 18 | choice = choice - 1 19 | if choice == 0 then 20 | spnr:spawn() 21 | end 22 | end 23 | self.time = 0 24 | end 25 | end 26 | 27 | function SpawnSystem:onAdd(e) 28 | self.levelState.spawners[e] = true 29 | self.levelState.spawnerCount = self.levelState.spawnerCount + 1 30 | end 31 | 32 | function SpawnSystem:onRemove(e) 33 | self.levelState.spawners[e] = false 34 | self.levelState.spawnerCount = self.levelState.spawnerCount - 1 35 | end 36 | 37 | return SpawnSystem 38 | -------------------------------------------------------------------------------- /src/entities/Bullet.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local Explosion = require "src.entities.Explosion" 3 | 4 | local Bullet = class("Bullet") 5 | 6 | Bullet.sprite = assets.img_bullet 7 | 8 | function Bullet:init(x, y, direction) 9 | self.speed = 400 + math.random() * 40 10 | self.pos = {x = x, y = y} 11 | self.vel = {x = self.speed * math.cos(direction), y = self.speed * math.sin(direction)} 12 | self.offset = {x = 4, y = 4} 13 | self.hitbox = {w = 4, h = 4} 14 | self.bullet = true 15 | self.drot = math.random() - 0.5 16 | self.rot = direction 17 | self.isBullet = true 18 | self.isSolid = true 19 | self.gravity = 1300 20 | self.bg = true 21 | end 22 | 23 | function Bullet:explode() 24 | world:remove(self) 25 | world:add(Explosion(self.pos.x - 32, self.pos.y - 60)) 26 | assets.snd_thud:play() 27 | end 28 | 29 | function Bullet:update(dt) 30 | self.rot = self.rot + self.drot * dt * 10 31 | end 32 | 33 | function Bullet:onCollision(col) 34 | self:explode() 35 | if col.other.isEnemy and col.other.gotHit then 36 | col.other:gotHit() 37 | end 38 | end 39 | 40 | 41 | return Bullet 42 | -------------------------------------------------------------------------------- /src/systems/SpriteSystem.lua: -------------------------------------------------------------------------------- 1 | local SpriteSystem = tiny.processingSystem(class "SpriteSystem") 2 | SpriteSystem.isDrawSystem = true 3 | 4 | function SpriteSystem:init(camera, layerFlag) 5 | self.camera = camera 6 | self.filter = tiny.requireAll("sprite", "pos", layerFlag) 7 | end 8 | 9 | function SpriteSystem:preProcess(dt) 10 | self.camera:apply() 11 | end 12 | 13 | function SpriteSystem:postProcess(dt) 14 | self.camera:remove() 15 | love.graphics.setColor(1, 1, 1, 1) 16 | end 17 | 18 | function SpriteSystem:process(e, dt) 19 | local an = e.animation 20 | local alpha = e.alpha or 1 21 | local pos, sprite, scale, rot, offset = e.pos, e.sprite, e.scale, e.rot, e.offset 22 | local sx, sy, r, ox, oy = scale and scale.x or 1, scale and scale.y or 1, rot or 0, offset and offset.x or 0, offset and offset.y or 0 23 | love.graphics.setColor(1, 1, 1, math.max(0, math.min(1, alpha))) 24 | if an then 25 | an.flippedH = e.flippedH or false 26 | an.flippedV = e.flippedV or false 27 | an:update(dt) 28 | an:draw(sprite, pos.x, pos.y, r, sx, sy, ox, oy) 29 | else 30 | love.graphics.draw(sprite, pos.x, pos.y, r, sx, sy, ox, oy) 31 | end 32 | if e.draw then 33 | e:draw(dt) 34 | end 35 | end 36 | 37 | return SpriteSystem 38 | -------------------------------------------------------------------------------- /src/assets.lua: -------------------------------------------------------------------------------- 1 | local assets = {} 2 | 3 | love.graphics.setDefaultFilter("nearest", "nearest") 4 | 5 | assets.img_cat = love.graphics.newImage("assets/cat.png") 6 | assets.img_catandcannon = love.graphics.newImage("assets/catandcannon.png") 7 | assets.img_gun = love.graphics.newImage("assets/gun.png") 8 | assets.img_bullet = love.graphics.newImage("assets/bullet.png") 9 | assets.img_explosion = love.graphics.newImage("assets/explosion.png") 10 | assets.img_pig = love.graphics.newImage("assets/pig.png") 11 | assets.img_spawner = love.graphics.newImage("assets/spawner.png") 12 | 13 | assets.snd_catjump = love.audio.newSource("assets/catjump.wav", "static") 14 | assets.snd_cannon = love.audio.newSource("assets/cannon.wav", "static") 15 | assets.snd_thud = love.audio.newSource("assets/thud.wav", "static") 16 | assets.snd_meow = love.audio.newSource("assets/meow.ogg", "static") 17 | assets.snd_oink = love.audio.newSource("assets/oink.ogg", "static") 18 | assets.snd_yay = love.audio.newSource("assets/yay.wav", "static") 19 | 20 | assets.snd_music = love.audio.newSource("assets/music.ogg", "stream") 21 | 22 | assets.fnt_hud = love.graphics.newFont("assets/font.ttf", 48) 23 | assets.fnt_smallhud = love.graphics.newFont("assets/font.ttf", 32) 24 | assets.fnt_reallysmallhud = love.graphics.newFont("assets/font.ttf", 24) 25 | 26 | 27 | return assets 28 | -------------------------------------------------------------------------------- /src/systems/PlatformingSystem.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | 3 | local PlatformingSystem = tiny.processingSystem(class "PlatformingSystem") 4 | 5 | PlatformingSystem.filter = tiny.requireAll("pos", "vel", "platforming") 6 | 7 | function PlatformingSystem:process(e, dt) 8 | local pos = e.pos 9 | local vel = e.vel 10 | local platforming = e.platforming 11 | local acceleration = platforming.acceleration 12 | local friction = platforming.friction 13 | local speed = platforming.speed 14 | local direction = platforming.direction 15 | e.flippedH = direction == 'l' 16 | 17 | if platforming.moving then 18 | if direction == 'l' then 19 | vel.x = math.max(-speed, vel.x - acceleration * dt) 20 | elseif direction == 'r' then 21 | vel.x = math.min(speed, vel.x + acceleration * dt) 22 | end 23 | elseif e.grounded then 24 | if vel.x > 0 then 25 | vel.x = math.max(0, vel.x - friction * dt) 26 | elseif vel.x < 0 then 27 | vel.x = math.min(0, vel.x + friction * dt) 28 | end 29 | end 30 | 31 | if platforming.jumping and e.grounded then 32 | vel.y = -platforming.jump 33 | e.grounded = false 34 | assets.snd_catjump:play() 35 | end 36 | 37 | e.animation = platforming.moving and e.animation_walk or e.animation_stand 38 | end 39 | 40 | return PlatformingSystem 41 | -------------------------------------------------------------------------------- /src/systems/PlayerControlSystem.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local Bullet = require "src.entities.Bullet" 3 | 4 | local PlayerControlSystem = tiny.processingSystem(class "PlayerControlSystem") 5 | 6 | PlayerControlSystem.filter = tiny.requireAll("controlable") 7 | 8 | function PlayerControlSystem:process(e, dt) 9 | local vel = e.vel 10 | local p = e.platforming 11 | local l, r, u = love.keyboard.isDown('a'), love.keyboard.isDown('d'), love.keyboard.isDown('w') 12 | local gl, gr = love.keyboard.isDown('left'), love.keyboard.isDown('right') 13 | local fire = love.keyboard.isDown('down') 14 | 15 | e.shotTimer = math.max(0, e.shotTimer - dt) 16 | 17 | if l and not r then 18 | p.moving = true 19 | if p.direction == 'r' then 20 | e.gunAngle = math.pi * 3 - e.gunAngle 21 | end 22 | p.direction = 'l' 23 | elseif r and not l then 24 | p.moving = true 25 | if p.direction == 'l' then 26 | e.gunAngle = math.pi * 3 - e.gunAngle 27 | end 28 | p.direction = 'r' 29 | else 30 | p.moving = false 31 | end 32 | 33 | p.jumping = u 34 | 35 | if gr and not gl then 36 | e.gunAngle = math.min(2 * math.pi, e.gunAngle + 8 * dt) 37 | elseif gl and not gr then 38 | e.gunAngle = math.max(math.pi, e.gunAngle - 8 * dt) 39 | end 40 | 41 | if e.hasGun and fire and e.shotTimer == 0 then 42 | local dx = e.platforming.direction == 'l' and 2 or -2 43 | local bullet = Bullet(e.pos.x + 16 + dx, e.pos.y + 9, e.gunAngle) 44 | assets.snd_cannon:play() 45 | world:add(bullet) 46 | e.shotTimer = e.shotInterval 47 | end 48 | end 49 | 50 | return PlayerControlSystem 51 | -------------------------------------------------------------------------------- /src/entities/Pig.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local anim8 = require "lib.anim8" 3 | local Explosion = require "src.entities.Explosion" 4 | local gamestate = require "lib.gamestate" 5 | 6 | local Pig = class "Pig" 7 | 8 | Pig.sprite = assets.img_pig 9 | 10 | function Pig:init(x, y, target) 11 | self.pos = {x = x, y = y} 12 | self.vel = {x = 0, y = 0} 13 | self.gravity = 1300 14 | 15 | self.isAlive = true 16 | self.isEnemy = true 17 | self.isSolid = true 18 | 19 | self.platforming = { 20 | acceleration = 1000, 21 | speed = 60, 22 | jump = 250, 23 | friction = 2000, 24 | direction = 'r' 25 | } 26 | 27 | self.ai = { 28 | 29 | } 30 | 31 | self.hitbox = {w = 30, h = 21} 32 | self.health = 50 33 | self.maxHealth = 50 34 | 35 | local g = anim8.newGrid(30, 21, assets.img_pig:getWidth(), assets.img_pig:getHeight()) 36 | self.animation_stand = anim8.newAnimation(g('1-1', 1), 0.1) 37 | self.animation_walk = anim8.newAnimation(g('2-5', 1), 0.1) 38 | self.animation = self.animation_stand 39 | self.fg = true 40 | end 41 | 42 | function Pig:gotHit() 43 | if self.isAlive then 44 | self.isAlive = nil 45 | self.lifetime = 0.25 46 | self.fadeTime = 0.25 47 | self.alpha = 1 48 | self.ai = nil 49 | self.platforming.moving = false 50 | self.vel.y = -300 51 | self.vel.x = 0 52 | assets.snd_oink:play() 53 | assets.snd_yay:play() 54 | world:add(self) 55 | gamestate.current().score = gamestate.current().score + 1 56 | end 57 | end 58 | 59 | return Pig 60 | -------------------------------------------------------------------------------- /src/states/Intro.lua: -------------------------------------------------------------------------------- 1 | local gamestate = require "lib.gamestate" 2 | local Level = require "src.states.Level" 3 | local TimerEvent = require "src.entities.TimerEvent" 4 | local ScreenSplash = require "src.entities.ScreenSplash" 5 | local TransitionScreen = require "src.entities.TransitionScreen" 6 | local Level = require "src.states.Level" 7 | local assets = require "src.assets" 8 | local sti = require "lib.sti" 9 | local gamera = require "lib.gamera" 10 | 11 | local Intro = class "Intro" 12 | 13 | local controltext = [[ 14 | Controls: 15 | Move - WASD 16 | Rotate Cannon - Arrow Keys 17 | Fire - Down 18 | Toggle Fullscreen - \ 19 | Toggle Music - M 20 | Pause - P 21 | Escape - Quit 22 | ]] 23 | 24 | function Intro:load() 25 | 26 | tileMap = sti("assets/intro.lua") 27 | local w, h = tileMap.tilewidth * tileMap.width, tileMap.tileheight * tileMap.height 28 | local camera = gamera.new(0, 0, w, h) 29 | camera:setPosition(600, 600) 30 | camera:setScale(2) 31 | 32 | self.time = 0 33 | self.world = tiny.world( 34 | require ("src.systems.DrawBackgroundSystem")(140/255, 205/255, 1), 35 | require ("src.systems.FadeSystem")(), 36 | require ("src.systems.LifetimeSystem")(), 37 | require ("src.systems.TileMapRenderSystem")(camera, tileMap), 38 | require ("src.systems.SpriteSystem")(camera, "bg"), 39 | require ("src.systems.SpriteSystem")(camera, "fg"), 40 | require ("src.systems.HudSystem")("hudBg"), 41 | require ("src.systems.HudSystem")("hudFg"), 42 | TransitionScreen(), 43 | ScreenSplash(0.5, 0.2, "Commando Kibbles"), 44 | ScreenSplash(0, 0, "Created by bakpakin for Ludum Dare 32", 300, assets.fnt_reallysmallhud, "left", 20, 20), 45 | ScreenSplash(0.5, 0.36, "Press Space to Start", 500, assets.fnt_smallhud), 46 | ScreenSplash(0.5, 0.45, controltext, 800, assets.fnt_reallysmallhud) 47 | ) 48 | _G.world = self.world 49 | _G.camera = camera 50 | end 51 | 52 | function Intro:update(dt) 53 | self.time = self.time + dt 54 | if love.keyboard.isDown("space") and self.time > 0.55 then 55 | world:add(TransitionScreen(true, Level("assets/lvl1.lua"))) 56 | end 57 | end 58 | 59 | function Intro:draw() 60 | 61 | end 62 | 63 | return Intro 64 | -------------------------------------------------------------------------------- /lib/sti/graphics.lua: -------------------------------------------------------------------------------- 1 | local lg = love.graphics 2 | local graphics = { isCreated = lg and true or false } 3 | 4 | function graphics.newSpriteBatch(...) 5 | if graphics.isCreated then 6 | return lg.newSpriteBatch(...) 7 | end 8 | end 9 | 10 | function graphics.newCanvas(...) 11 | if graphics.isCreated then 12 | return lg.newCanvas(...) 13 | end 14 | end 15 | 16 | function graphics.newImage(...) 17 | if graphics.isCreated then 18 | return lg.newImage(...) 19 | end 20 | end 21 | 22 | function graphics.newQuad(...) 23 | if graphics.isCreated then 24 | return lg.newQuad(...) 25 | end 26 | end 27 | 28 | function graphics.getCanvas(...) 29 | if graphics.isCreated then 30 | return lg.getCanvas(...) 31 | end 32 | end 33 | 34 | function graphics.setCanvas(...) 35 | if graphics.isCreated then 36 | return lg.setCanvas(...) 37 | end 38 | end 39 | 40 | function graphics.clear(...) 41 | if graphics.isCreated then 42 | return lg.clear(...) 43 | end 44 | end 45 | 46 | function graphics.push(...) 47 | if graphics.isCreated then 48 | return lg.push(...) 49 | end 50 | end 51 | 52 | function graphics.origin(...) 53 | if graphics.isCreated then 54 | return lg.origin(...) 55 | end 56 | end 57 | 58 | function graphics.scale(...) 59 | if graphics.isCreated then 60 | return lg.scale(...) 61 | end 62 | end 63 | 64 | function graphics.translate(...) 65 | if graphics.isCreated then 66 | return lg.translate(...) 67 | end 68 | end 69 | 70 | function graphics.pop(...) 71 | if graphics.isCreated then 72 | return lg.pop(...) 73 | end 74 | end 75 | 76 | function graphics.draw(...) 77 | if graphics.isCreated then 78 | return lg.draw(...) 79 | end 80 | end 81 | 82 | function graphics.rectangle(...) 83 | if graphics.isCreated then 84 | return lg.rectangle(...) 85 | end 86 | end 87 | 88 | function graphics.getColor(...) 89 | if graphics.isCreated then 90 | return lg.getColor(...) 91 | end 92 | end 93 | 94 | function graphics.setColor(...) 95 | if graphics.isCreated then 96 | return lg.setColor(...) 97 | end 98 | end 99 | 100 | function graphics.line(...) 101 | if graphics.isCreated then 102 | return lg.line(...) 103 | end 104 | end 105 | 106 | function graphics.polygon(...) 107 | if graphics.isCreated then 108 | return lg.polygon(...) 109 | end 110 | end 111 | 112 | function graphics.getWidth() 113 | if graphics.isCreated then 114 | return lg.getWidth() 115 | end 116 | return 0 117 | end 118 | 119 | function graphics.getHeight() 120 | if graphics.isCreated then 121 | return lg.getHeight() 122 | end 123 | return 0 124 | end 125 | 126 | return graphics -------------------------------------------------------------------------------- /src/entities/Player.lua: -------------------------------------------------------------------------------- 1 | local assets = require "src.assets" 2 | local anim8 = require "lib.anim8" 3 | local Bullet = require "src.entities.Bullet" 4 | local TimerEvent = require "src.entities.TimerEvent" 5 | local ScreenSplash = require "src.entities.ScreenSplash" 6 | local gamestate = require "lib.gamestate" 7 | 8 | local Player = class("Player") 9 | 10 | function Player:draw(dt) 11 | if self.hasGun then 12 | local p = self.animation.position 13 | local dy = (p ~= 2 and p ~= 3) and 0 or -1 14 | local dx = self.platforming.direction == 'l' and 2 or -2 15 | love.graphics.draw(assets.img_gun, self.pos.x + 16 + dx, self.pos.y + 10 + dy, self.gunAngle - math.pi / 4) 16 | end 17 | end 18 | 19 | function Player:onHit() 20 | self.isAlive = nil 21 | self.lifetime = 0.25 22 | self.fadeTime = 0.25 23 | self.alpha = 1 24 | self.ai = nil 25 | self.platforming.moving = false 26 | self.vel.y = -300 27 | self.vel.x = (math.random() - 0.5) * 400 28 | self.controlable = nil 29 | assets.snd_meow:play() 30 | world:add(self) 31 | local n = gamestate.current().score 32 | local message = "You Died." 33 | if n == 0 then message = "You Failed Pretty Hard." 34 | elseif n < 10 then message = "You Killed Some Pigs and They Killed you Back." 35 | elseif n < 30 then message = "That's a lot of Bacon." 36 | elseif n < 100 then message = "You a crazy Pig Killer." 37 | else message = "Pigpocolypse." end 38 | 39 | world:add(TimerEvent(1.2, function() world:add(ScreenSplash(0.5, 0.4, message .. " Press Space to Try Again.", 800)) end)) 40 | gamestate.current().isSpawning = false 41 | gamestate.current().restartOnSpace = true 42 | end 43 | 44 | function Player:onCollision(col) 45 | if self.isAlive and col.other.isEnemy and col.other.isAlive then 46 | self:onHit() 47 | end 48 | end 49 | 50 | function Player:init(args) 51 | self.cameraTrack = {xoffset = 16, yoffset = -35} 52 | self.pos = {x = args.x, y = args.y} 53 | self.vel = {x = 0, y = 0} 54 | self.gravity = 1300 55 | self.platforming = { 56 | acceleration = 1000, 57 | speed = 130, 58 | jump = 380, 59 | friction = 2000, 60 | direction = 'r' 61 | } 62 | self.isAlive = true 63 | self.isPlayer = true 64 | self.isSolid = true 65 | self.controlable = true 66 | self.hitbox = {w = 32, h = 32} 67 | self.checkCollisions = true 68 | self.sprite = assets.img_catandcannon 69 | self.fg = true 70 | local g = anim8.newGrid(32, 32, assets.img_cat:getWidth(), assets.img_cat:getHeight()) 71 | self.animation_stand = anim8.newAnimation(g('1-1', 1), 0.1) 72 | self.animation_walk = anim8.newAnimation(g('2-5', 1), 0.1) 73 | self.animation = self.animation_stand 74 | self.health = 100 75 | self.maxHealth = 100 76 | self.shotTimer = 0 77 | self.shotInterval = 0.45 78 | self.gunAngle = 2 * math.pi 79 | self.hasGun = true 80 | end 81 | 82 | return Player 83 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | class = require "lib.30log" 2 | tiny = require "lib.tiny" 3 | gamestate = require "lib.gamestate" -- slightly modified to play nice;y with 30log 4 | local Intro = require "src.states.Intro" 5 | local Level = require "src.states.Level" 6 | 7 | local assets = nil 8 | 9 | local beholder = require "lib.beholder" 10 | 11 | local paused = false 12 | local pauseNextFrame = false 13 | local pauseCanvas = nil 14 | local drawFilter = tiny.requireAll('isDrawSystem') 15 | local updateFilter = tiny.rejectAny('isDrawSystem') 16 | 17 | function love.keypressed(k) 18 | beholder.trigger("keypress", k) 19 | end 20 | 21 | function love.keyreleased(k) 22 | beholder.trigger("keyrelease", k) 23 | end 24 | 25 | function love.load() 26 | love.mouse.setVisible(false) 27 | assets = require "src.assets" -- load assets 28 | gamestate.registerEvents() 29 | gamestate.switch(Intro()) 30 | assets.snd_music:play() 31 | assets.snd_music:setLooping(true) 32 | love.resize(love.graphics.getWidth(), love.graphics.getHeight()) 33 | end 34 | 35 | function love.draw() 36 | if paused then 37 | love.graphics.setColor(0.35, 0.35, 0.35, 1) 38 | love.graphics.draw(pauseCanvas, 0, 0) 39 | love.graphics.setColor(1, 1, 1, 1) 40 | love.graphics.setFont(assets.fnt_hud) 41 | love.graphics.printf("Paused - P to Resume", love.graphics.getWidth() * 0.5 - 125, love.graphics.getHeight() * 0.4, 250, "center") 42 | else 43 | local dt = love.timer.getDelta() 44 | if world then 45 | world:update(dt, drawFilter) 46 | end 47 | end 48 | end 49 | 50 | function love.update(dt) 51 | if world then 52 | world:update(dt, updateFilter) 53 | end 54 | local s = gamestate.current() 55 | if s and s.restartOnSpace and love.keyboard.isDown("space") then 56 | local TransitionScreen = require "src.entities.TransitionScreen" 57 | world:add(TransitionScreen(true, Intro())) 58 | end 59 | end 60 | 61 | function love.resize(w, h) 62 | pauseCanvas = love.graphics.newCanvas(w, h) 63 | if camera then 64 | camera:setWindow(0, 0, w, h) 65 | end 66 | if paused then 67 | love.graphics.setCanvas(pauseCanvas) 68 | world:update(0) 69 | love.graphics.setCanvas() 70 | end 71 | beholder.trigger('resize', w, h) 72 | end 73 | 74 | -- quitting 75 | beholder.observe("keypress", "escape", love.event.quit) 76 | 77 | -- pausing 78 | beholder.observe("keypress", "p", function() 79 | paused = not paused 80 | if paused then 81 | love.graphics.setCanvas(pauseCanvas) 82 | world:update(0) 83 | love.graphics.setCanvas() 84 | end 85 | end) 86 | 87 | -- toggle music 88 | beholder.observe("keypress", "m", function() 89 | local vol = assets.snd_music:getVolume() 90 | if vol == 0 then 91 | assets.snd_music:setVolume(1) 92 | else 93 | assets.snd_music:setVolume(0) 94 | end 95 | end) 96 | 97 | -- toggle fullscreen 98 | beholder.observe("keypress", "\\", function() 99 | local fs = love.window.getFullscreen() 100 | if fs then 101 | love.window.setMode(900, 600, {resizable = true}) 102 | else 103 | local w, h = love.window.getDesktopDimensions() 104 | love.window.setMode(w, h, {fullscreen = true}) 105 | end 106 | love.resize(love.graphics.getWidth(), love.graphics.getHeight()) 107 | end) 108 | -------------------------------------------------------------------------------- /src/systems/BumpPhysicsSystem.lua: -------------------------------------------------------------------------------- 1 | local BumpPhysicsSystem = tiny.processingSystem(class "BumpPhysicsSystem") 2 | 3 | function BumpPhysicsSystem:init(bumpWorld) 4 | self.bumpWorld = bumpWorld 5 | end 6 | 7 | BumpPhysicsSystem.filter = tiny.requireAll("pos", "vel", "hitbox") 8 | 9 | local oneWayPrefix = "o" 10 | oneWayPrefix = oneWayPrefix:byte(1) 11 | local function collisionFilter(e1, e2) 12 | if e1.isPlayer then 13 | if e2.isBullet then return nil end 14 | if e2.isEnemy then return 'cross' end 15 | elseif e1.isEnemy then 16 | if e2.isBullet then return nil end 17 | if e2.isEnemy then return nil end 18 | if e2.isPlayer then return 'cross' end 19 | elseif e1.isBullet then 20 | if e2.isPlayer or e2.isBullet then return nil end 21 | end 22 | if e1.isSolid then 23 | if type(e2) == "string" then -- tile collision 24 | if e2:byte(1) == oneWayPrefix then -- one way tile 25 | if e1.isBullet then 26 | return 'onewayplatformTouch' 27 | else 28 | return 'onewayplatform' 29 | end 30 | else 31 | return 'slide' 32 | end 33 | elseif e2.isSolid then 34 | return 'slide' 35 | elseif e2.isBouncy then 36 | return 'bounce' 37 | else 38 | return 'cross' 39 | end 40 | end 41 | return nil 42 | end 43 | 44 | function BumpPhysicsSystem:process(e, dt) 45 | local pos = e.pos 46 | local vel = e.vel 47 | local gravity = e.gravity or 0 48 | vel.y = vel.y + gravity * dt 49 | local cols, len 50 | pos.x, pos.y, cols, len = self.bumpWorld:move(e, pos.x + vel.x * dt, pos.y + vel.y * dt, collisionFilter) 51 | e.grounded = false 52 | for i = 1, len do 53 | local col = cols[i] 54 | local collided = true 55 | if col.type == "touch" then 56 | vel.x, vel.y = 0, 0 57 | elseif col.type == "slide" then 58 | if col.normal.x == 0 then 59 | vel.y = 0 60 | if col.normal.y < 0 then 61 | e.grounded = true 62 | end 63 | else 64 | vel.x = 0 65 | end 66 | elseif col.type == "onewayplatform" then 67 | if col.didTouch then 68 | vel.y = 0 69 | e.grounded = true 70 | else 71 | collided = false 72 | end 73 | elseif col.type == "onewayplatformTouch" then 74 | if col.didTouch then 75 | vel.y = 0 76 | e.grounded = true 77 | else 78 | collided = false 79 | end 80 | elseif col.type == "bounce" then 81 | if col.normal.x == 0 then 82 | vel.y = -vel.y 83 | e.grounded = true 84 | else 85 | vel.x = -vel.x 86 | end 87 | end 88 | 89 | if e.onCollision and collided then 90 | e:onCollision(col) 91 | end 92 | end 93 | end 94 | 95 | function BumpPhysicsSystem:onAdd(e) 96 | local pos = e.pos 97 | local hitbox = e.hitbox 98 | self.bumpWorld:add(e, pos.x, pos.y, hitbox.w, hitbox.h) 99 | end 100 | 101 | function BumpPhysicsSystem:onRemove(e) 102 | self.bumpWorld:remove(e) 103 | end 104 | 105 | return BumpPhysicsSystem 106 | -------------------------------------------------------------------------------- /lib/gamestate.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | Except as contained in this notice, the name(s) of the above copyright holders 12 | shall not be used in advertising or otherwise to promote the sale, use or 13 | other dealings in this Software without prior written authorization. 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | ]]-- 22 | 23 | local function __NULL__() end 24 | 25 | -- default gamestate produces error on every callback 26 | local state_init = setmetatable({leave = __NULL__}, 27 | {__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end}) 28 | local stack = {state_init} 29 | 30 | local GS = {} 31 | function GS.new(t) return t or {} end -- constructor - deprecated! 32 | 33 | function GS.switch(to, ...) 34 | assert(to, "Missing argument: Gamestate to switch to") 35 | assert(to ~= GS, "Can't call switch with colon operator") 36 | local pre = stack[#stack] 37 | ;(pre.leave or __NULL__)(pre) 38 | ;(to.load or __NULL__)(to) 39 | to.load = nil 40 | stack[#stack] = to 41 | return (to.enter or __NULL__)(to, pre, ...) 42 | end 43 | 44 | function GS.push(to, ...) 45 | assert(to, "Missing argument: Gamestate to switch to") 46 | assert(to ~= GS, "Can't call push with colon operator") 47 | local pre = stack[#stack] 48 | ;(to.load or __NULL__)(to) -- modified to use load instead of init so as 49 | -- to not interfere with 30log. 50 | to.load = nil 51 | stack[#stack+1] = to 52 | return (to.enter or __NULL__)(to, pre, ...) 53 | end 54 | 55 | function GS.pop(...) 56 | assert(#stack > 1, "No more states to pop!") 57 | local pre, to = stack[#stack], stack[#stack-1] 58 | stack[#stack] = nil 59 | ;(pre.leave or __NULL__)(pre) 60 | return (to.resume or __NULL__)(to, pre, ...) 61 | end 62 | 63 | function GS.current() 64 | return stack[#stack] 65 | end 66 | 67 | local all_callbacks = { 68 | 'draw', 'errhand', 'focus', 'keypressed', 'keyreleased', 'mousefocus', 69 | 'mousemoved', 'mousepressed', 'mousereleased', 'quit', 'resize', 70 | 'textinput', 'threaderror', 'update', 'visible', 'gamepadaxis', 71 | 'gamepadpressed', 'gamepadreleased', 'joystickadded', 'joystickaxis', 72 | 'joystickhat', 'joystickpressed', 'joystickreleased', 'joystickremoved' 73 | } 74 | 75 | function GS.registerEvents(callbacks) 76 | local registry = {} 77 | callbacks = callbacks or all_callbacks 78 | for _, f in ipairs(callbacks) do 79 | registry[f] = love[f] or __NULL__ 80 | love[f] = function(...) 81 | registry[f](...) 82 | return GS[f](...) 83 | end 84 | end 85 | end 86 | 87 | -- forward any undefined functions 88 | setmetatable(GS, {__index = function(_, func) 89 | return function(...) 90 | return (stack[#stack][func] or __NULL__)(stack[#stack], ...) 91 | end 92 | end}) 93 | 94 | return GS 95 | -------------------------------------------------------------------------------- /src/states/Level.lua: -------------------------------------------------------------------------------- 1 | local sti = require("lib.sti") 2 | local bump = require("lib.bump") 3 | local gamera = require("lib.gamera") 4 | local TimerEvent = require "src.entities.TimerEvent" 5 | local ScreenSplash = require "src.entities.ScreenSplash" 6 | local TransitionScreen = require "src.entities.TransitionScreen" 7 | 8 | local Level = class "Level" 9 | 10 | local waveTable = {4, 10, 20, 50, 80, 100, 120, 150, 180, 200, 250} 11 | local waveSpawnSpeeds = {1, 1, 1.5, 2, 2, 2.2, 3, 3, 4, 5, 7} 12 | 13 | function Level:init(mappath) 14 | self.mappath = mappath 15 | end 16 | 17 | function Level:nextWave() 18 | self.wave = self.wave + 1 19 | self.enemiesKilled = 0 20 | self.totalEnemiesToKill = waveTable[self.wave] or math.huge 21 | self.enemiesSpawned = 0 22 | self.spawnInterval = 3 / (waveSpawnSpeeds[self.wave] or 10) 23 | self.isSpawning = true 24 | if self.wave == #waveTable + 1 then 25 | local splash = ScreenSplash(0.5, 0.5, "Kill Them ALL!!") 26 | world:add(TimerEvent(1, function() world:add(splash) end), 27 | TimerEvent(3, function() world:remove(splash) end)) 28 | end 29 | end 30 | 31 | function Level:load() 32 | local tileMap = sti(self.mappath) 33 | local bumpWorld = bump.newWorld(tileMap.tilewidth * 2) 34 | local w, h = tileMap.tilewidth * tileMap.width, tileMap.tileheight * tileMap.height 35 | local camera = gamera.new(0, 0, w, h) 36 | 37 | self.wave = 0 38 | self:nextWave() 39 | self.score = 0 40 | 41 | self.spawnerCount = 0 42 | self.spawners = {} 43 | 44 | self.tileMap = tileMap 45 | self.bumpWorld = bumpWorld 46 | self.camera = camera 47 | 48 | self.aiSystem = require ("src.systems.AISystem")() 49 | 50 | local r, g, b = tileMap.backgroundcolor[1], tileMap.backgroundcolor[2], tileMap.backgroundcolor[3] 51 | 52 | camera:setScale(2) 53 | local world = tiny.world( 54 | require ("src.systems.DrawBackgroundSystem")(r/255, g/255, b/255), 55 | require ("src.systems.UpdateSystem")(), 56 | require ("src.systems.PlayerControlSystem")(), 57 | self.aiSystem, 58 | require ("src.systems.FadeSystem")(), 59 | require ("src.systems.PlatformingSystem")(), 60 | require ("src.systems.BumpPhysicsSystem")(bumpWorld), 61 | require ("src.systems.CameraTrackingSystem")(camera), 62 | require ("src.systems.TileMapRenderSystem")(camera, tileMap), 63 | require ("src.systems.SpriteSystem")(camera, "bg"), 64 | require ("src.systems.SpriteSystem")(camera, "fg"), 65 | require ("src.systems.LifetimeSystem")(), 66 | require ("src.systems.HudSystem")("hudBg"), 67 | require ("src.systems.HudSystem")("hudFg"), 68 | require ("src.systems.WaveSystem")(self), 69 | require ("src.systems.SpawnSystem")(self), 70 | require ("src.entities.MainHud")(self), 71 | TransitionScreen() 72 | ) 73 | 74 | local player = nil 75 | 76 | for lindex, layer in ipairs(tileMap.layers) do 77 | if layer.properties.collidable == "true" then 78 | -- Entire layer 79 | if layer.type == "tilelayer" then 80 | local prefix = layer.properties.oneway == "true" and "o(" or "t(" 81 | for y, tiles in ipairs(layer.data) do 82 | for x, tile in pairs(tiles) do 83 | bumpWorld:add( 84 | prefix..layer.name..", "..x..", "..y..")", 85 | x * tileMap.tilewidth + tile.offset.x, 86 | y * tileMap.tileheight + tile.offset.y, 87 | tile.width, 88 | tile.height 89 | ) 90 | end 91 | end 92 | elseif layer.type == "imagelayer" then 93 | bumpWorld:add( 94 | layer.name, 95 | layer.x or 0, 96 | layer.y or 0, 97 | layer.width, 98 | layer.height 99 | ) 100 | end 101 | end 102 | 103 | if layer.type == "objectgroup" then 104 | for _, object in ipairs(layer.objects) do 105 | local ctor = require("src.entities." .. object.type) 106 | local e = ctor(object) 107 | if object.type == "Player" then 108 | player = e 109 | end 110 | world:add(e) 111 | end 112 | tileMap:removeLayer(lindex) 113 | end 114 | 115 | end 116 | 117 | self.aiSystem.target = player 118 | 119 | -- add ends to prevent objects from falling off the edge of the world. 120 | bumpWorld:add("_leftBlock", -16, 0, 16, h) 121 | bumpWorld:add("_rightBlock", w, 0, 16, h) 122 | bumpWorld:add("_topBlock", 0, -16, w, 16) 123 | 124 | -- globals 125 | _G.camera = camera 126 | _G.world = world 127 | end 128 | 129 | return Level 130 | -------------------------------------------------------------------------------- /lib/beholder.lua: -------------------------------------------------------------------------------- 1 | -- beholder.lua - v2.1.1 (2011-11) 2 | 3 | -- Copyright (c) 2011 Enrique García Cota 4 | -- 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: 5 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | -- 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 callback OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | 8 | local function copy(t) 9 | local c={} 10 | for i=1,#t do c[i]=t[i] end 11 | return c 12 | end 13 | 14 | local function hash2array(t) 15 | local arr, i = {}, 0 16 | for _,v in pairs(t) do 17 | i = i+1 18 | arr[i] = v 19 | end 20 | return arr, i 21 | end 22 | -- private Node class 23 | 24 | local nodesById = nil 25 | local root = nil 26 | 27 | local function newNode() 28 | return { callbacks = {}, children = setmetatable({}, {__mode="k"}) } 29 | end 30 | 31 | 32 | local function findNodeById(id) 33 | return nodesById[id] 34 | end 35 | 36 | local function findOrCreateChildNode(self, key) 37 | self.children[key] = self.children[key] or newNode() 38 | return self.children[key] 39 | end 40 | 41 | local function findOrCreateDescendantNode(self, keys) 42 | local node = self 43 | for i=1, #keys do 44 | node = findOrCreateChildNode(node, keys[i]) 45 | end 46 | return node 47 | end 48 | 49 | local function invokeNodeCallbacks(self, params) 50 | -- copy the hash into an array, for safety (self-erasures) 51 | local callbacks, count = hash2array(self.callbacks) 52 | for i=1,#callbacks do 53 | callbacks[i](unpack(params)) 54 | end 55 | return count 56 | end 57 | 58 | local function invokeAllNodeCallbacksInSubTree(self, params) 59 | local counter = invokeNodeCallbacks(self, params) 60 | for _,child in pairs(self.children) do 61 | counter = counter + invokeAllNodeCallbacksInSubTree(child, params) 62 | end 63 | return counter 64 | end 65 | 66 | local function invokeNodeCallbacksFromPath(self, path) 67 | local node = self 68 | local params = copy(path) 69 | local counter = invokeNodeCallbacks(node, params) 70 | 71 | for i=1, #path do 72 | node = node.children[path[i]] 73 | if not node then break end 74 | table.remove(params, 1) 75 | counter = counter + invokeNodeCallbacks(node, params) 76 | end 77 | 78 | return counter 79 | end 80 | 81 | local function addCallbackToNode(self, callback) 82 | local id = {} 83 | self.callbacks[id] = callback 84 | nodesById[id] = self 85 | return id 86 | end 87 | 88 | local function removeCallbackFromNode(self, id) 89 | self.callbacks[id] = nil 90 | nodesById[id] = nil 91 | end 92 | 93 | 94 | ------ beholder table 95 | 96 | local beholder = {} 97 | 98 | 99 | -- beholder private functions/vars 100 | 101 | local groups = nil 102 | local currentGroupId = nil 103 | 104 | local function addIdToCurrentGroup(id) 105 | if currentGroupId then 106 | groups[currentGroupId] = groups[currentGroupId] or setmetatable({}, {__mode="k"}) 107 | local group = groups[currentGroupId] 108 | group[#group + 1] = id 109 | end 110 | return id 111 | end 112 | 113 | local function stopObservingGroup(group) 114 | local count = #group 115 | for i=1,count do 116 | beholder.stopObserving(group[i]) 117 | end 118 | return count 119 | end 120 | 121 | local function falseIfZero(n) 122 | return n > 0 and n 123 | end 124 | 125 | local function extractEventAndCallbackFromParams(params) 126 | assert(#params > 0, "beholder.observe requires at least one parameter - the callback. You usually want to use two, i.e.: beholder.observe('EVENT', callback)") 127 | local callback = table.remove(params, #params) 128 | return params, callback 129 | end 130 | 131 | 132 | ------ Public interface 133 | 134 | function beholder.observe(...) 135 | local event, callback = extractEventAndCallbackFromParams({...}) 136 | local node = findOrCreateDescendantNode(root, event) 137 | return addIdToCurrentGroup(addCallbackToNode(node, callback)) 138 | end 139 | 140 | function beholder.stopObserving(id) 141 | local node = findNodeById(id) 142 | if node then removeCallbackFromNode(node, id) end 143 | 144 | local group, count = groups[id], 0 145 | if group then count = stopObservingGroup(group) end 146 | 147 | return (node or count > 0) and true or false 148 | end 149 | 150 | function beholder.group(groupId, f) 151 | assert(not currentGroupId, "beholder.group can not be nested!") 152 | currentGroupId = groupId 153 | f() 154 | currentGroupId = nil 155 | end 156 | 157 | function beholder.trigger(...) 158 | return falseIfZero( invokeNodeCallbacksFromPath(root, {...}) ) 159 | end 160 | 161 | function beholder.triggerAll(...) 162 | return falseIfZero( invokeAllNodeCallbacksInSubTree(root, {...}) ) 163 | end 164 | 165 | function beholder.reset() 166 | root = newNode() 167 | nodesById = setmetatable({}, {__mode="k"}) 168 | groups = {} 169 | currentGroupId = nil 170 | end 171 | 172 | beholder.reset() 173 | 174 | return beholder 175 | -------------------------------------------------------------------------------- /lib/sti/utils.lua: -------------------------------------------------------------------------------- 1 | -- Some utility functions that shouldn't be exposed. 2 | local utils = {} 3 | 4 | -- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286 5 | function utils.format_path(path) 6 | local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP' 7 | local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/') 8 | local k 9 | 10 | repeat -- /./ -> / 11 | path,k = path:gsub(np_pat2,'/') 12 | until k == 0 13 | 14 | repeat -- A/../ -> (empty) 15 | path,k = path:gsub(np_pat1,'') 16 | until k == 0 17 | 18 | if path == '' then path = '.' end 19 | 20 | return path 21 | end 22 | 23 | -- Compensation for scale/rotation shift 24 | function utils.compensate(tile, tileX, tileY, tileW, tileH) 25 | local origx = tileX 26 | local origy = tileY 27 | local compx = 0 28 | local compy = 0 29 | 30 | if tile.sx < 0 then compx = tileW end 31 | if tile.sy < 0 then compy = tileH end 32 | 33 | if tile.r > 0 then 34 | tileX = tileX + tileH - compy 35 | tileY = tileY + tileH + compx - tileW 36 | elseif tile.r < 0 then 37 | tileX = tileX + compy 38 | tileY = tileY - compx + tileH 39 | else 40 | tileX = tileX + compx 41 | tileY = tileY + compy 42 | end 43 | 44 | return tileX, tileY 45 | end 46 | 47 | -- Cache images in main STI module 48 | function utils.cache_image(sti, path, image) 49 | image = image or love.graphics.newImage(path) 50 | image:setFilter("nearest", "nearest") 51 | sti.cache[path] = image 52 | end 53 | 54 | -- We just don't know. 55 | function utils.get_tiles(imageW, tileW, margin, spacing) 56 | imageW = imageW - margin 57 | local n = 0 58 | 59 | while imageW >= tileW do 60 | imageW = imageW - tileW 61 | if n ~= 0 then imageW = imageW - spacing end 62 | if imageW >= 0 then n = n + 1 end 63 | end 64 | 65 | return n 66 | end 67 | 68 | -- Decompress tile layer data 69 | function utils.get_decompressed_data(data) 70 | local ffi = require "ffi" 71 | local d = {} 72 | local decoded = ffi.cast("uint32_t*", data) 73 | 74 | for i = 0, data:len() / ffi.sizeof("uint32_t") do 75 | table.insert(d, tonumber(decoded[i])) 76 | end 77 | 78 | return d 79 | end 80 | 81 | -- Convert a Tiled ellipse object to a LOVE polygon 82 | function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) 83 | local ceil = math.ceil 84 | local cos = math.cos 85 | local sin = math.sin 86 | 87 | local function calc_segments(segments) 88 | local function vdist(a, b) 89 | local c = { 90 | x = a.x - b.x, 91 | y = a.y - b.y, 92 | } 93 | 94 | return c.x * c.x + c.y * c.y 95 | end 96 | 97 | segments = segments or 64 98 | local vertices = {} 99 | 100 | local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) } 101 | 102 | local m 103 | if love and love.physics then 104 | m = love.physics.getMeter() 105 | else 106 | m = 32 107 | end 108 | 109 | for _, i in ipairs(v) do 110 | local angle = (i / segments) * math.pi * 2 111 | local px = x + w / 2 + cos(angle) * w / 2 112 | local py = y + h / 2 + sin(angle) * h / 2 113 | 114 | table.insert(vertices, { x = px / m, y = py / m }) 115 | end 116 | 117 | local dist1 = vdist(vertices[1], vertices[2]) 118 | local dist2 = vdist(vertices[3], vertices[4]) 119 | 120 | -- Box2D threshold 121 | if dist1 < 0.0025 or dist2 < 0.0025 then 122 | return calc_segments(segments-2) 123 | end 124 | 125 | return segments 126 | end 127 | 128 | local segments = calc_segments(max_segments) 129 | local vertices = {} 130 | 131 | table.insert(vertices, { x = x + w / 2, y = y + h / 2 }) 132 | 133 | for i = 0, segments do 134 | local angle = (i / segments) * math.pi * 2 135 | local px = x + w / 2 + cos(angle) * w / 2 136 | local py = y + h / 2 + sin(angle) * h / 2 137 | 138 | table.insert(vertices, { x = px, y = py }) 139 | end 140 | 141 | return vertices 142 | end 143 | 144 | function utils.rotate_vertex(map, vertex, x, y, cos, sin) 145 | if map.orientation == "isometric" then 146 | x, y = utils.convert_isometric_to_screen(map, x, y) 147 | vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y) 148 | end 149 | 150 | vertex.x = vertex.x - x 151 | vertex.y = vertex.y - y 152 | 153 | return 154 | x + cos * vertex.x - sin * vertex.y, 155 | y + sin * vertex.x + cos * vertex.y 156 | end 157 | 158 | --- Project isometric position to cartesian position 159 | function utils.convert_isometric_to_screen(map, x, y) 160 | local mapH = map.height 161 | local tileW = map.tilewidth 162 | local tileH = map.tileheight 163 | local tileX = x / tileH 164 | local tileY = y / tileH 165 | local offsetX = mapH * tileW / 2 166 | 167 | return 168 | (tileX - tileY) * tileW / 2 + offsetX, 169 | (tileX + tileY) * tileH / 2 170 | end 171 | 172 | function utils.hex_to_color(hex) 173 | if hex:sub(1, 1) == "#" then 174 | hex = hex:sub(2) 175 | end 176 | 177 | return { 178 | r = tonumber(hex:sub(1, 2), 16) / 255, 179 | g = tonumber(hex:sub(3, 4), 16) / 255, 180 | b = tonumber(hex:sub(5, 6), 16) / 255 181 | } 182 | end 183 | 184 | function utils.pixel_function(_, _, r, g, b, a) 185 | local mask = utils._TC 186 | 187 | if r == mask.r and 188 | g == mask.g and 189 | b == mask.b then 190 | return r, g, b, 0 191 | end 192 | 193 | return r, g, b, a 194 | end 195 | 196 | function utils.fix_transparent_color(tileset, path) 197 | local image_data = love.image.newImageData(path) 198 | tileset.image = love.graphics.newImage(image_data) 199 | 200 | if tileset.transparentcolor then 201 | utils._TC = utils.hex_to_color(tileset.transparentcolor) 202 | 203 | image_data:mapPixel(utils.pixel_function) 204 | tileset.image = love.graphics.newImage(image_data) 205 | end 206 | end 207 | 208 | return utils 209 | -------------------------------------------------------------------------------- /lib/30log.lua: -------------------------------------------------------------------------------- 1 | local next, assert, pairs, type, tostring, setmetatable, baseMt, _instances, _classes, _class = next, assert, pairs, type, tostring, setmetatable, {}, setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}) 2 | local function assert_call_from_class(class, method) assert(_classes[class], ('Wrong method call. Expected class:%s.'):format(method)) end; local function assert_call_from_instance(instance, method) assert(_instances[instance], ('Wrong method call. Expected instance:%s.'):format(method)) end 3 | local function bind(f, v) return function(...) return f(v, ...) end end 4 | local default_filter = function() return true end 5 | local function deep_copy(t, dest, aType) t = t or {}; local r = dest or {}; for k,v in pairs(t) do if aType ~= nil and type(v) == aType then r[k] = (type(v) == 'table') and ((_classes[v] or _instances[v]) and v or deep_copy(v)) or v elseif aType == nil then r[k] = (type(v) == 'table') and k~= '__index' and ((_classes[v] or _instances[v]) and v or deep_copy(v)) or v end; end return r end 6 | local function instantiate(call_init,self,...) assert_call_from_class(self, 'new(...) or class(...)'); local instance = {class = self}; _instances[instance] = tostring(instance); deep_copy(self, instance, 'table') 7 | instance.__index, instance.__subclasses, instance.__instances, instance.mixins = nil, nil, nil, nil; setmetatable(instance,self); if call_init and self.init then if type(self.init) == 'table' then deep_copy(self.init, instance) else self.init(instance, ...) end end; return instance 8 | end 9 | local function extend(self, name, extra_params) 10 | assert_call_from_class(self, 'extend(...)'); local heir = {}; _classes[heir] = tostring(heir); self.__subclasses[heir] = true; deep_copy(extra_params, deep_copy(self, heir)) 11 | heir.name, heir.__index, heir.super, heir.mixins = extra_params and extra_params.name or name, heir, self, {}; return setmetatable(heir,self) 12 | end 13 | baseMt = { __call = function (self,...) return self:new(...) end, __tostring = function(self,...) 14 | if _instances[self] then return ("instance of '%s' (%s)"):format(rawget(self.class,'name') or '?', _instances[self]) end; return _classes[self] and ("class '%s' (%s)"):format(rawget(self,'name') or '?', _classes[self]) or self end 15 | }; _classes[baseMt] = tostring(baseMt); setmetatable(baseMt, {__tostring = baseMt.__tostring}) 16 | local class = {isClass = function(t) return not not _classes[t] end, isInstance = function(t) return not not _instances[t] end} 17 | _class = function(name, attr) local c = deep_copy(attr); _classes[c] = tostring(c) 18 | c.name, c.__tostring, c.__call, c.new, c.create, c.extend, c.__index, c.mixins, c.__instances, c.__subclasses = name or c.name, baseMt.__tostring, baseMt.__call, bind(instantiate, true), bind(instantiate, false), extend, c, setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}), setmetatable({},{__mode = 'k'}) 19 | c.subclasses = function(self, filter, ...) assert_call_from_class(self, 'subclasses(class)'); filter = filter or default_filter; local subclasses = {}; for class in pairs(_classes) do if class ~= baseMt and class:subclassOf(self) and filter(class,...) then subclasses[#subclasses + 1] = class end end; return subclasses end 20 | c.instances = function(self, filter, ...) assert_call_from_class(self, 'instances(class)'); filter = filter or default_filter; local instances = {}; for instance in pairs(_instances) do if instance:instanceOf(self) and filter(instance, ...) then instances[#instances + 1] = instance end end; return instances end 21 | c.subclassOf = function(self, superclass) assert_call_from_class(self, 'subclassOf(superclass)'); assert(class.isClass(superclass), 'Wrong argument given to method "subclassOf()". Expected a class.'); local super = self.super; while super do if super == superclass then return true end; super = super.super end; return false end 22 | c.classOf = function(self, subclass) assert_call_from_class(self, 'classOf(subclass)'); assert(class.isClass(subclass), 'Wrong argument given to method "classOf()". Expected a class.'); return subclass:subclassOf(self) end 23 | c.instanceOf = function(self, fromclass) assert_call_from_instance(self, 'instanceOf(class)'); assert(class.isClass(fromclass), 'Wrong argument given to method "instanceOf()". Expected a class.'); return ((self.class == fromclass) or (self.class:subclassOf(fromclass))) end 24 | c.cast = function(self, toclass) assert_call_from_instance(self, 'instanceOf(class)'); assert(class.isClass(toclass), 'Wrong argument given to method "cast()". Expected a class.'); setmetatable(self, toclass); self.class = toclass; return self end 25 | c.with = function(self,...) assert_call_from_class(self, 'with(mixin)'); for _, mixin in ipairs({...}) do assert(self.mixins[mixin] ~= true, ('Attempted to include a mixin which was already included in %s'):format(tostring(self))); self.mixins[mixin] = true; deep_copy(mixin, self, 'function') end return self end 26 | c.includes = function(self, mixin) assert_call_from_class(self,'includes(mixin)'); return not not (self.mixins[mixin] or (self.super and self.super:includes(mixin))) end 27 | c.without = function(self, ...) assert_call_from_class(self, 'without(mixin)'); for _, mixin in ipairs({...}) do 28 | assert(self.mixins[mixin] == true, ('Attempted to remove a mixin which is not included in %s'):format(tostring(self))); local classes = self:subclasses(); classes[#classes + 1] = self 29 | for _, class in ipairs(classes) do for method_name, method in pairs(mixin) do if type(method) == 'function' then class[method_name] = nil end end end; self.mixins[mixin] = nil end; return self end; return setmetatable(c, baseMt) end 30 | class._DESCRIPTION = '30 lines library for object orientation in Lua'; class._VERSION = '30log v1.2.0'; class._URL = 'http://github.com/Yonaba/30log'; class._LICENSE = 'MIT LICENSE ' 31 | return setmetatable(class,{__call = function(_,...) return _class(...) end }) 32 | -------------------------------------------------------------------------------- /lib/sti/plugins/bump.lua: -------------------------------------------------------------------------------- 1 | --- Bump.lua plugin for STI 2 | -- @module bump.lua 3 | -- @author David Serrano (BobbyJones|FrenchFryLord) 4 | -- @copyright 2016 5 | -- @license MIT/X11 6 | 7 | local lg = require((...):gsub('plugins.bump', 'graphics')) 8 | 9 | return { 10 | bump_LICENSE = "MIT/X11", 11 | bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation", 12 | bump_VERSION = "3.1.6.1", 13 | bump_DESCRIPTION = "Bump hooks for STI.", 14 | 15 | --- Adds each collidable tile to the Bump world. 16 | -- @param world The Bump world to add objects to. 17 | -- @return collidables table containing the handles to the objects in the Bump world. 18 | bump_init = function(map, world) 19 | local collidables = {} 20 | 21 | for _, tileset in ipairs(map.tilesets) do 22 | for _, tile in ipairs(tileset.tiles) do 23 | local gid = tileset.firstgid + tile.id 24 | 25 | if map.tileInstances[gid] then 26 | for _, instance in ipairs(map.tileInstances[gid]) do 27 | -- Every object in every instance of a tile 28 | if tile.objectGroup then 29 | for _, object in ipairs(tile.objectGroup.objects) do 30 | if object.properties.collidable == true then 31 | local t = { 32 | name = object.name, 33 | type = object.type, 34 | x = instance.x + map.offsetx + object.x, 35 | y = instance.y + map.offsety + object.y, 36 | width = object.width, 37 | height = object.height, 38 | layer = instance.layer, 39 | properties = object.properties 40 | 41 | } 42 | 43 | world:add(t, t.x, t.y, t.width, t.height) 44 | table.insert(collidables, t) 45 | end 46 | end 47 | end 48 | 49 | -- Every instance of a tile 50 | if tile.properties and tile.properties.collidable == true then 51 | local t = { 52 | x = instance.x + map.offsetx, 53 | y = instance.y + map.offsety, 54 | width = map.tilewidth, 55 | height = map.tileheight, 56 | layer = instance.layer, 57 | properties = tile.properties 58 | } 59 | 60 | world:add(t, t.x, t.y, t.width, t.height) 61 | table.insert(collidables, t) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | for _, layer in ipairs(map.layers) do 69 | -- Entire layer 70 | if layer.properties.collidable == true then 71 | if layer.type == "tilelayer" then 72 | for y, tiles in ipairs(layer.data) do 73 | for x, tile in pairs(tiles) do 74 | 75 | if tile.objectGroup then 76 | for _, object in ipairs(tile.objectGroup.objects) do 77 | if object.properties.collidable == true then 78 | local t = { 79 | name = object.name, 80 | type = object.type, 81 | x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x, 82 | y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y, 83 | width = object.width, 84 | height = object.height, 85 | layer = layer, 86 | properties = object.properties 87 | } 88 | 89 | world:add(t, t.x, t.y, t.width, t.height) 90 | table.insert(collidables, t) 91 | end 92 | end 93 | end 94 | 95 | 96 | local t = { 97 | x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx, 98 | y = (y-1) * map.tileheight + tile.offset.y + map.offsety, 99 | width = tile.width, 100 | height = tile.height, 101 | layer = layer, 102 | properties = tile.properties 103 | } 104 | 105 | world:add(t, t.x, t.y, t.width, t.height) 106 | table.insert(collidables, t) 107 | end 108 | end 109 | elseif layer.type == "imagelayer" then 110 | world:add(layer, layer.x, layer.y, layer.width, layer.height) 111 | table.insert(collidables, layer) 112 | end 113 | end 114 | 115 | -- individual collidable objects in a layer that is not "collidable" 116 | -- or whole collidable objects layer 117 | if layer.type == "objectgroup" then 118 | for _, obj in ipairs(layer.objects) do 119 | if layer.properties.collidable == true or obj.properties.collidable == true then 120 | if obj.shape == "rectangle" then 121 | local t = { 122 | name = obj.name, 123 | type = obj.type, 124 | x = obj.x + map.offsetx, 125 | y = obj.y + map.offsety, 126 | width = obj.width, 127 | height = obj.height, 128 | layer = layer, 129 | properties = obj.properties 130 | } 131 | 132 | if obj.gid then 133 | t.y = t.y - obj.height 134 | end 135 | 136 | world:add(t, t.x, t.y, t.width, t.height) 137 | table.insert(collidables, t) 138 | end -- TODO implement other object shapes? 139 | end 140 | end 141 | end 142 | 143 | end 144 | map.bump_collidables = collidables 145 | end, 146 | 147 | --- Remove layer 148 | -- @param index to layer to be removed 149 | -- @param world bump world the holds the tiles 150 | -- @param tx Translate on X 151 | -- @param ty Translate on Y 152 | -- @param sx Scale on X 153 | -- @param sy Scale on Y 154 | bump_removeLayer = function(map, index, world) 155 | local layer = assert(map.layers[index], "Layer not found: " .. index) 156 | local collidables = map.bump_collidables 157 | 158 | -- Remove collision objects 159 | for i = #collidables, 1, -1 do 160 | local obj = collidables[i] 161 | 162 | if obj.layer == layer 163 | and ( 164 | layer.properties.collidable == true 165 | or obj.properties.collidable == true 166 | ) then 167 | world:remove(obj) 168 | table.remove(collidables, i) 169 | end 170 | end 171 | end, 172 | 173 | --- Draw bump collisions world. 174 | -- @param world bump world holding the tiles geometry 175 | -- @param tx Translate on X 176 | -- @param ty Translate on Y 177 | -- @param sx Scale on X 178 | -- @param sy Scale on Y 179 | bump_draw = function(map, world, tx, ty, sx, sy) 180 | lg.push() 181 | lg.scale(sx or 1, sy or sx or 1) 182 | lg.translate(math.floor(tx or 0), math.floor(ty or 0)) 183 | 184 | for _, collidable in pairs(map.bump_collidables) do 185 | lg.rectangle("line", world:getRect(collidable)) 186 | end 187 | 188 | lg.pop() 189 | end 190 | } 191 | 192 | --- Custom Properties in Tiled are used to tell this plugin what to do. 193 | -- @table Properties 194 | -- @field collidable set to true, can be used on any Layer, Tile, or Object 195 | -------------------------------------------------------------------------------- /lib/gamera.lua: -------------------------------------------------------------------------------- 1 | -- gamera.lua v1.0.1 2 | 3 | -- Copyright (c) 2012 Enrique García Cota 4 | -- 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: 5 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | -- 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. 7 | -- Based on YaciCode, from Julien Patte and LuaObject, from Sebastien Rocca-Serra 8 | 9 | local gamera = {} 10 | 11 | -- Private attributes and methods 12 | 13 | local gameraMt = {__index = gamera} 14 | local abs, min, max = math.abs, math.min, math.max 15 | 16 | local function clamp(x, minX, maxX) 17 | return x < minX and minX or (x>maxX and maxX or x) 18 | end 19 | 20 | local function checkNumber(value, name) 21 | if type(value) ~= 'number' then 22 | error(name .. " must be a number (was: " .. tostring(value) .. ")") 23 | end 24 | end 25 | 26 | local function checkPositiveNumber(value, name) 27 | if type(value) ~= 'number' or value <=0 then 28 | error(name .. " must be a positive number (was: " .. tostring(value) ..")") 29 | end 30 | end 31 | 32 | local function checkAABB(l,t,w,h) 33 | checkNumber(l, "l") 34 | checkNumber(t, "t") 35 | checkPositiveNumber(w, "w") 36 | checkPositiveNumber(h, "h") 37 | end 38 | 39 | local function getVisibleArea(self, scale) 40 | scale = scale or self.scale 41 | local sin, cos = abs(self.sin), abs(self.cos) 42 | local w,h = self.w / scale, self.h / scale 43 | w,h = cos*w + sin*h, sin*w + cos*h 44 | return min(w,self.ww), min(h, self.wh) 45 | end 46 | 47 | local function cornerTransform(self, x,y) 48 | local scale, sin, cos = self.scale, self.sin, self.cos 49 | x,y = x - self.x, y - self.y 50 | x,y = -cos*x + sin*y, -sin*x - cos*y 51 | return self.x - (x/scale + self.l), self.y - (y/scale + self.t) 52 | end 53 | 54 | local function adjustPosition(self) 55 | local wl,wt,ww,wh = self.wl, self.wt, self.ww, self.wh 56 | local w,h = getVisibleArea(self) 57 | local w2,h2 = w*0.5, h*0.5 58 | 59 | local left, right = wl + w2, wl + ww - w2 60 | local top, bottom = wt + h2, wt + wh - h2 61 | 62 | self.x, self.y = clamp(self.x, left, right), clamp(self.y, top, bottom) 63 | end 64 | 65 | local function adjustScale(self) 66 | local w,h,ww,wh = self.w, self.h, self.ww, self.wh 67 | local rw,rh = getVisibleArea(self, 1) -- rotated frame: area around the window, rotated without scaling 68 | local sx,sy = rw/ww, rh/wh -- vert/horiz scale: minimun scales that the window needs to occupy the world 69 | local rscale = max(sx,sy) 70 | 71 | self.scale = max(self.scale, rscale) 72 | end 73 | 74 | -- Public interface 75 | 76 | function gamera.new(l,t,w,h) 77 | 78 | local sw,sh = love.graphics.getWidth(), love.graphics.getHeight() 79 | 80 | local cam = setmetatable({ 81 | x=0, y=0, 82 | scale=1, 83 | angle=0, sin=math.sin(0), cos=math.cos(0), 84 | l=0, t=0, w=sw, h=sh, w2=sw*0.5, h2=sh*0.5 85 | }, gameraMt) 86 | 87 | cam:setWorld(l,t,w,h) 88 | 89 | return cam 90 | end 91 | 92 | function gamera:setWorld(l,t,w,h) 93 | checkAABB(l,t,w,h) 94 | 95 | self.wl, self.wt, self.ww, self.wh = l,t,w,h 96 | 97 | adjustPosition(self) 98 | end 99 | 100 | function gamera:setWindow(l,t,w,h) 101 | checkAABB(l,t,w,h) 102 | 103 | self.l, self.t, self.w, self.h, self.w2, self.h2 = l,t,w,h, w*0.5, h*0.5 104 | 105 | adjustPosition(self) 106 | end 107 | 108 | function gamera:setPosition(x,y) 109 | checkNumber(x, "x") 110 | checkNumber(y, "y") 111 | 112 | self.x, self.y = x,y 113 | 114 | adjustPosition(self) 115 | end 116 | 117 | function gamera:setScale(scale) 118 | checkNumber(scale, "scale") 119 | 120 | self.scale = scale 121 | 122 | adjustScale(self) 123 | adjustPosition(self) 124 | end 125 | 126 | function gamera:setAngle(angle) 127 | checkNumber(angle, "angle") 128 | 129 | self.angle = angle 130 | self.cos, self.sin = math.cos(angle), math.sin(angle) 131 | 132 | adjustScale(self) 133 | adjustPosition(self) 134 | end 135 | 136 | function gamera:getWorld() 137 | return self.wl, self.wt, self.ww, self.wh 138 | end 139 | 140 | function gamera:getWindow() 141 | return self.l, self.t, self.w, self.h 142 | end 143 | 144 | function gamera:getPosition() 145 | return self.x, self.y 146 | end 147 | 148 | function gamera:getScale() 149 | return self.scale 150 | end 151 | 152 | function gamera:getAngle() 153 | return self.angle 154 | end 155 | 156 | function gamera:getVisible() 157 | local w,h = getVisibleArea(self) 158 | return self.x - w*0.5, self.y - h*0.5, w, h 159 | end 160 | 161 | function gamera:getVisibleCorners() 162 | local x,y,w2,h2 = self.x, self.y, self.w2, self.h2 163 | 164 | local x1,y1 = cornerTransform(self, x-w2,y-h2) 165 | local x2,y2 = cornerTransform(self, x+w2,y-h2) 166 | local x3,y3 = cornerTransform(self, x+w2,y+h2) 167 | local x4,y4 = cornerTransform(self, x-w2,y+h2) 168 | 169 | return x1,y1,x2,y2,x3,y3,x4,y4 170 | end 171 | 172 | -- modified here 173 | function gamera:apply() 174 | love.graphics.setScissor(self:getWindow()) 175 | 176 | love.graphics.push() 177 | local scale = self.scale 178 | love.graphics.scale(scale) 179 | love.graphics.translate((self.w2 + self.l) / scale, (self.h2+self.t) / scale) 180 | love.graphics.rotate(-self.angle) 181 | love.graphics.translate(-self.x, -self.y) 182 | end 183 | 184 | function gamera:remove() 185 | love.graphics.pop() 186 | love.graphics.setScissor() 187 | end 188 | 189 | function gamera:draw(f) 190 | self:apply() 191 | f(self:getVisible()) 192 | self:remove() 193 | end 194 | -- to here 195 | 196 | function gamera:toWorld(x,y) 197 | local scale, sin, cos = self.scale, self.sin, self.cos 198 | x,y = (x - self.w2 - self.l) / scale, (y - self.h2 - self.t) / scale 199 | x,y = cos*x - sin*y, sin*x + cos*y 200 | return x + self.x, y + self.y 201 | end 202 | 203 | function gamera:toScreen(x,y) 204 | local scale, sin, cos = self.scale, self.sin, self.cos 205 | x,y = x - self.x, y - self.y 206 | x,y = cos*x + sin*y, -sin*x + cos*y 207 | return scale * x + self.w2 + self.l, scale * y + self.h2 + self.t 208 | end 209 | 210 | return gamera 211 | -------------------------------------------------------------------------------- /lib/anim8.lua: -------------------------------------------------------------------------------- 1 | local anim8 = { 2 | _VERSION = 'anim8 v2.1.0', 3 | _DESCRIPTION = 'An animation library for LÖVE', 4 | _URL = 'https://github.com/kikito/anim8', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | Copyright (c) 2011 Enrique García Cota 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | ]] 25 | } 26 | 27 | local Grid = {} 28 | 29 | local _frames = {} 30 | 31 | local function assertPositiveInteger(value, name) 32 | if type(value) ~= 'number' then error(("%s should be a number, was %q"):format(name, tostring(value))) end 33 | if value < 1 then error(("%s should be a positive number, was %d"):format(name, value)) end 34 | if value ~= math.floor(value) then error(("%s should be an integer, was %d"):format(name, value)) end 35 | end 36 | 37 | local function createFrame(self, x, y) 38 | local fw, fh = self.frameWidth, self.frameHeight 39 | return love.graphics.newQuad( 40 | self.left + (x-1) * fw + x * self.border, 41 | self.top + (y-1) * fh + y * self.border, 42 | fw, 43 | fh, 44 | self.imageWidth, 45 | self.imageHeight 46 | ) 47 | end 48 | 49 | local function getGridKey(...) 50 | return table.concat( {...} ,'-' ) 51 | end 52 | 53 | local function getOrCreateFrame(self, x, y) 54 | if x < 1 or x > self.width or y < 1 or y > self.height then 55 | error(("There is no frame for x=%d, y=%d"):format(x, y)) 56 | end 57 | local key = self._key 58 | _frames[key] = _frames[key] or {} 59 | _frames[key][x] = _frames[key][x] or {} 60 | _frames[key][x][y] = _frames[key][x][y] or createFrame(self, x, y) 61 | return _frames[key][x][y] 62 | end 63 | 64 | local function parseInterval(str) 65 | if type(str) == "number" then return str,str,1 end 66 | str = str:gsub('%s', '') -- remove spaces 67 | local min, max = str:match("^(%d+)-(%d+)$") 68 | assert(min and max, ("Could not parse interval from %q"):format(str)) 69 | min, max = tonumber(min), tonumber(max) 70 | local step = min <= max and 1 or -1 71 | return min, max, step 72 | end 73 | 74 | function Grid:getFrames(...) 75 | local result, args = {}, {...} 76 | local minx, maxx, stepx, miny, maxy, stepy 77 | 78 | for i=1, #args, 2 do 79 | minx, maxx, stepx = parseInterval(args[i]) 80 | miny, maxy, stepy = parseInterval(args[i+1]) 81 | for y = miny, maxy, stepy do 82 | for x = minx, maxx, stepx do 83 | result[#result+1] = getOrCreateFrame(self,x,y) 84 | end 85 | end 86 | end 87 | 88 | return result 89 | end 90 | 91 | local Gridmt = { 92 | __index = Grid, 93 | __call = Grid.getFrames 94 | } 95 | 96 | local function newGrid(frameWidth, frameHeight, imageWidth, imageHeight, left, top, border) 97 | assertPositiveInteger(frameWidth, "frameWidth") 98 | assertPositiveInteger(frameHeight, "frameHeight") 99 | assertPositiveInteger(imageWidth, "imageWidth") 100 | assertPositiveInteger(imageHeight, "imageHeight") 101 | 102 | left = left or 0 103 | top = top or 0 104 | border = border or 0 105 | 106 | local key = getGridKey(frameWidth, frameHeight, imageWidth, imageHeight, left, top, border) 107 | 108 | local grid = setmetatable( 109 | { frameWidth = frameWidth, 110 | frameHeight = frameHeight, 111 | imageWidth = imageWidth, 112 | imageHeight = imageHeight, 113 | left = left, 114 | top = top, 115 | border = border, 116 | width = math.floor(imageWidth/frameWidth), 117 | height = math.floor(imageHeight/frameHeight), 118 | _key = key 119 | }, 120 | Gridmt 121 | ) 122 | return grid 123 | end 124 | 125 | ----------------------------------------------------------- 126 | 127 | local Animation = {} 128 | 129 | local function cloneArray(arr) 130 | local result = {} 131 | for i=1,#arr do result[i] = arr[i] end 132 | return result 133 | end 134 | 135 | local function parseDurations(durations, frameCount) 136 | local result = {} 137 | if type(durations) == 'number' then 138 | for i=1,frameCount do result[i] = durations end 139 | else 140 | local min, max, step 141 | for key,duration in pairs(durations) do 142 | assert(type(duration) == 'number', "The value [" .. tostring(duration) .. "] should be a number") 143 | min, max, step = parseInterval(key) 144 | for i = min,max,step do result[i] = duration end 145 | end 146 | end 147 | 148 | if #result < frameCount then 149 | error("The durations table has length of " .. tostring(#result) .. ", but it should be >= " .. tostring(frameCount)) 150 | end 151 | 152 | return result 153 | end 154 | 155 | local function parseIntervals(durations) 156 | local result, time = {0},0 157 | for i=1,#durations do 158 | time = time + durations[i] 159 | result[i+1] = time 160 | end 161 | return result, time 162 | end 163 | 164 | local Animationmt = { __index = Animation } 165 | local nop = function() end 166 | 167 | local function newAnimation(frames, durations, onLoop) 168 | local td = type(durations); 169 | if (td ~= 'number' or durations <= 0) and td ~= 'table' then 170 | error("durations must be a positive number. Was " .. tostring(durations) ) 171 | end 172 | onLoop = onLoop or nop 173 | durations = parseDurations(durations, #frames) 174 | local intervals, totalDuration = parseIntervals(durations) 175 | return setmetatable({ 176 | frames = cloneArray(frames), 177 | durations = durations, 178 | intervals = intervals, 179 | totalDuration = totalDuration, 180 | onLoop = onLoop, 181 | timer = 0, 182 | position = 1, 183 | status = "playing", 184 | flippedH = false, 185 | flippedV = false 186 | }, 187 | Animationmt 188 | ) 189 | end 190 | 191 | function Animation:clone() 192 | local newAnim = newAnimation(self.frames, self.durations, self.onLoop) 193 | newAnim.flippedH, newAnim.flippedV = self.flippedH, self.flippedV 194 | return newAnim 195 | end 196 | 197 | function Animation:flipH() 198 | self.flippedH = not self.flippedH 199 | return self 200 | end 201 | 202 | function Animation:flipV() 203 | self.flippedV = not self.flippedV 204 | return self 205 | end 206 | 207 | local function seekFrameIndex(intervals, timer) 208 | local high, low, i = #intervals-1, 1, 1 209 | 210 | while(low <= high) do 211 | i = math.floor((low + high) / 2) 212 | if timer > intervals[i+1] then low = i + 1 213 | elseif timer <= intervals[i] then high = i - 1 214 | else 215 | return i 216 | end 217 | end 218 | 219 | return i 220 | end 221 | 222 | function Animation:update(dt) 223 | if self.status ~= "playing" then return end 224 | 225 | self.timer = self.timer + dt 226 | local loops = math.floor(self.timer / self.totalDuration) 227 | if loops ~= 0 then 228 | self.timer = self.timer - self.totalDuration * loops 229 | local f = type(self.onLoop) == 'function' and self.onLoop or self[self.onLoop] 230 | f(self, loops) 231 | end 232 | 233 | self.position = seekFrameIndex(self.intervals, self.timer) 234 | end 235 | 236 | function Animation:pause() 237 | self.status = "paused" 238 | end 239 | 240 | function Animation:gotoFrame(position) 241 | self.position = position 242 | self.timer = self.intervals[self.position] 243 | end 244 | 245 | function Animation:pauseAtEnd() 246 | self.position = #self.frames 247 | self.timer = self.totalDuration 248 | self:pause() 249 | end 250 | 251 | function Animation:pauseAtStart() 252 | self.position = 1 253 | self.timer = 0 254 | self:pause() 255 | end 256 | 257 | function Animation:resume() 258 | self.status = "playing" 259 | end 260 | 261 | function Animation:draw(image, x, y, r, sx, sy, ox, oy, ...) 262 | local frame = self.frames[self.position] 263 | if self.flippedH or self.flippedV then 264 | r,sx,sy,ox,oy = r or 0, sx or 1, sy or 1, ox or 0, oy or 0 265 | local _,_,w,h = frame:getViewport() 266 | 267 | if self.flippedH then 268 | sx = sx * -1 269 | ox = w - ox 270 | end 271 | if self.flippedV then 272 | sy = sy * -1 273 | oy = h - oy 274 | end 275 | end 276 | love.graphics.draw(image, frame, x, y, r, sx, sy, ox, oy, ...) 277 | end 278 | 279 | ----------------------------------------------------------- 280 | 281 | anim8.newGrid = newGrid 282 | anim8.newAnimation = newAnimation 283 | 284 | return anim8 -------------------------------------------------------------------------------- /lib/sti/plugins/box2d.lua: -------------------------------------------------------------------------------- 1 | --- Box2D plugin for STI 2 | -- @module box2d 3 | -- @author Landon Manning 4 | -- @copyright 2017 5 | -- @license MIT/X11 6 | 7 | local utils = require((...):gsub('plugins.box2d', 'utils')) 8 | local lg = require((...):gsub('plugins.box2d', 'graphics')) 9 | 10 | return { 11 | box2d_LICENSE = "MIT/X11", 12 | box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation", 13 | box2d_VERSION = "2.3.2.6", 14 | box2d_DESCRIPTION = "Box2D hooks for STI.", 15 | 16 | --- Initialize Box2D physics world. 17 | -- @param world The Box2D world to add objects to. 18 | box2d_init = function(map, world) 19 | assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.") 20 | 21 | local body = love.physics.newBody(world, map.offsetx, map.offsety) 22 | local collision = { 23 | body = body, 24 | } 25 | 26 | local function addObjectToWorld(objshape, vertices, userdata, object) 27 | local shape 28 | 29 | if objshape == "polyline" then 30 | if #vertices == 4 then 31 | shape = love.physics.newEdgeShape(unpack(vertices)) 32 | else 33 | shape = love.physics.newChainShape(false, unpack(vertices)) 34 | end 35 | else 36 | shape = love.physics.newPolygonShape(unpack(vertices)) 37 | end 38 | 39 | local currentBody = body 40 | 41 | if userdata.properties.dynamic == true then 42 | currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic') 43 | end 44 | 45 | local fixture = love.physics.newFixture(currentBody, shape) 46 | 47 | fixture:setUserData(userdata) 48 | 49 | if userdata.properties.sensor == true then 50 | fixture:setSensor(true) 51 | end 52 | 53 | local obj = { 54 | object = object, 55 | body = currentBody, 56 | shape = shape, 57 | fixture = fixture, 58 | } 59 | 60 | table.insert(collision, obj) 61 | end 62 | 63 | local function getPolygonVertices(object) 64 | local vertices = {} 65 | for _, vertex in ipairs(object.polygon) do 66 | table.insert(vertices, vertex.x) 67 | table.insert(vertices, vertex.y) 68 | end 69 | 70 | return vertices 71 | end 72 | 73 | local function calculateObjectPosition(object, tile) 74 | local o = { 75 | shape = object.shape, 76 | x = (object.dx or object.x) + map.offsetx, 77 | y = (object.dy or object.y) + map.offsety, 78 | w = object.width, 79 | h = object.height, 80 | polygon = object.polygon or object.polyline or object.ellipse or object.rectangle 81 | } 82 | 83 | local userdata = { 84 | object = o, 85 | properties = object.properties 86 | } 87 | 88 | if o.shape == "rectangle" then 89 | o.r = object.rotation or 0 90 | local cos = math.cos(math.rad(o.r)) 91 | local sin = math.sin(math.rad(o.r)) 92 | local oy = 0 93 | 94 | if object.gid then 95 | local tileset = map.tilesets[map.tiles[object.gid].tileset] 96 | local lid = object.gid - tileset.firstgid 97 | local t = {} 98 | 99 | -- This fixes a height issue 100 | o.y = o.y + map.tiles[object.gid].offset.y 101 | oy = tileset.tileheight 102 | 103 | for _, tt in ipairs(tileset.tiles) do 104 | if tt.id == lid then 105 | t = tt 106 | break 107 | end 108 | end 109 | 110 | if t.objectGroup then 111 | for _, obj in ipairs(t.objectGroup.objects) do 112 | -- Every object in the tile 113 | calculateObjectPosition(obj, object) 114 | end 115 | 116 | return 117 | else 118 | o.w = map.tiles[object.gid].width 119 | o.h = map.tiles[object.gid].height 120 | end 121 | end 122 | 123 | o.polygon = { 124 | { x=o.x+0, y=o.y+0 }, 125 | { x=o.x+o.w, y=o.y+0 }, 126 | { x=o.x+o.w, y=o.y+o.h }, 127 | { x=o.x+0, y=o.y+o.h } 128 | } 129 | 130 | for _, vertex in ipairs(o.polygon) do 131 | vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy) 132 | end 133 | 134 | local vertices = getPolygonVertices(o) 135 | addObjectToWorld(o.shape, vertices, userdata, tile or object) 136 | elseif o.shape == "ellipse" then 137 | if not o.polygon then 138 | o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h) 139 | end 140 | local vertices = getPolygonVertices(o) 141 | local triangles = love.math.triangulate(vertices) 142 | 143 | for _, triangle in ipairs(triangles) do 144 | addObjectToWorld(o.shape, triangle, userdata, tile or object) 145 | end 146 | elseif o.shape == "polygon" then 147 | local vertices = getPolygonVertices(o) 148 | local triangles = love.math.triangulate(vertices) 149 | 150 | for _, triangle in ipairs(triangles) do 151 | addObjectToWorld(o.shape, triangle, userdata, tile or object) 152 | end 153 | elseif o.shape == "polyline" then 154 | local vertices = getPolygonVertices(o) 155 | addObjectToWorld(o.shape, vertices, userdata, tile or object) 156 | end 157 | end 158 | 159 | for _, tile in pairs(map.tiles) do 160 | if map.tileInstances[tile.gid] then 161 | for _, instance in ipairs(map.tileInstances[tile.gid]) do 162 | -- Every object in every instance of a tile 163 | if tile.objectGroup then 164 | for _, object in ipairs(tile.objectGroup.objects) do 165 | if object.properties.collidable == true then 166 | object.dx = instance.x + object.x 167 | object.dy = instance.y + object.y 168 | calculateObjectPosition(object, instance) 169 | end 170 | end 171 | end 172 | 173 | -- Every instance of a tile 174 | if tile.properties.collidable == true then 175 | local object = { 176 | shape = "rectangle", 177 | x = instance.x, 178 | y = instance.y, 179 | width = map.tilewidth, 180 | height = map.tileheight, 181 | properties = tile.properties 182 | } 183 | 184 | calculateObjectPosition(object, instance) 185 | end 186 | end 187 | end 188 | end 189 | 190 | for _, layer in ipairs(map.layers) do 191 | -- Entire layer 192 | if layer.properties.collidable == true then 193 | if layer.type == "tilelayer" then 194 | for gid, tiles in pairs(map.tileInstances) do 195 | local tile = map.tiles[gid] 196 | local tileset = map.tilesets[tile.tileset] 197 | 198 | for _, instance in ipairs(tiles) do 199 | if instance.layer == layer then 200 | local object = { 201 | shape = "rectangle", 202 | x = instance.x, 203 | y = instance.y, 204 | width = tileset.tilewidth, 205 | height = tileset.tileheight, 206 | properties = tile.properties 207 | } 208 | 209 | calculateObjectPosition(object, instance) 210 | end 211 | end 212 | end 213 | elseif layer.type == "objectgroup" then 214 | for _, object in ipairs(layer.objects) do 215 | calculateObjectPosition(object) 216 | end 217 | elseif layer.type == "imagelayer" then 218 | local object = { 219 | shape = "rectangle", 220 | x = layer.x or 0, 221 | y = layer.y or 0, 222 | width = layer.width, 223 | height = layer.height, 224 | properties = layer.properties 225 | } 226 | 227 | calculateObjectPosition(object) 228 | end 229 | end 230 | 231 | -- Individual objects 232 | if layer.type == "objectgroup" then 233 | for _, object in ipairs(layer.objects) do 234 | if object.properties.collidable == true then 235 | calculateObjectPosition(object) 236 | end 237 | end 238 | end 239 | end 240 | 241 | map.box2d_collision = collision 242 | end, 243 | 244 | --- Remove Box2D fixtures and shapes from world. 245 | -- @param index The index or name of the layer being removed 246 | box2d_removeLayer = function(map, index) 247 | local layer = assert(map.layers[index], "Layer not found: " .. index) 248 | local collision = map.box2d_collision 249 | 250 | -- Remove collision objects 251 | for i = #collision, 1, -1 do 252 | local obj = collision[i] 253 | 254 | if obj.object.layer == layer then 255 | obj.fixture:destroy() 256 | table.remove(collision, i) 257 | end 258 | end 259 | end, 260 | 261 | --- Draw Box2D physics world. 262 | -- @param tx Translate on X 263 | -- @param ty Translate on Y 264 | -- @param sx Scale on X 265 | -- @param sy Scale on Y 266 | box2d_draw = function(map, tx, ty, sx, sy) 267 | local collision = map.box2d_collision 268 | 269 | lg.push() 270 | lg.scale(sx or 1, sy or sx or 1) 271 | lg.translate(math.floor(tx or 0), math.floor(ty or 0)) 272 | 273 | for _, obj in ipairs(collision) do 274 | local points = {obj.body:getWorldPoints(obj.shape:getPoints())} 275 | local shape_type = obj.shape:getType() 276 | 277 | if shape_type == "edge" or shape_type == "chain" then 278 | love.graphics.line(points) 279 | elseif shape_type == "polygon" then 280 | love.graphics.polygon("line", points) 281 | else 282 | error("sti box2d plugin does not support "..shape_type.." shapes") 283 | end 284 | end 285 | 286 | lg.pop() 287 | end, 288 | } 289 | 290 | --- Custom Properties in Tiled are used to tell this plugin what to do. 291 | -- @table Properties 292 | -- @field collidable set to true, can be used on any Layer, Tile, or Object 293 | -- @field sensor set to true, can be used on any Tile or Object that is also collidable 294 | -- @field dynamic set to true, can be used on any Tile or Object 295 | -------------------------------------------------------------------------------- /lib/bump.lua: -------------------------------------------------------------------------------- 1 | local bump = { 2 | _VERSION = 'bump v3.1.7', 3 | _URL = 'https://github.com/kikito/bump.lua', 4 | _DESCRIPTION = 'A collision detection library for Lua', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2014 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | ------------------------------------------ 32 | -- Auxiliary functions 33 | ------------------------------------------ 34 | local DELTA = 1e-10 -- floating-point margin of error 35 | 36 | local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max 37 | 38 | local function sign(x) 39 | if x > 0 then return 1 end 40 | if x == 0 then return 0 end 41 | return -1 42 | end 43 | 44 | local function nearest(x, a, b) 45 | if abs(a - x) < abs(b - x) then return a else return b end 46 | end 47 | 48 | local function assertType(desiredType, value, name) 49 | if type(value) ~= desiredType then 50 | error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')') 51 | end 52 | end 53 | 54 | local function assertIsPositiveNumber(value, name) 55 | if type(value) ~= 'number' or value <= 0 then 56 | error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')') 57 | end 58 | end 59 | 60 | local function assertIsRect(x,y,w,h) 61 | assertType('number', x, 'x') 62 | assertType('number', y, 'y') 63 | assertIsPositiveNumber(w, 'w') 64 | assertIsPositiveNumber(h, 'h') 65 | end 66 | 67 | local defaultFilter = function() 68 | return 'slide' 69 | end 70 | 71 | ------------------------------------------ 72 | -- Rectangle functions 73 | ------------------------------------------ 74 | 75 | local function rect_getNearestCorner(x,y,w,h, px, py) 76 | return nearest(px, x, x+w), nearest(py, y, y+h) 77 | end 78 | 79 | -- This is a generalized implementation of the liang-barsky algorithm, which also returns 80 | -- the normals of the sides where the segment intersects. 81 | -- Returns nil if the segment never touches the rect 82 | -- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge 83 | local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2) 84 | ti1, ti2 = ti1 or 0, ti2 or 1 85 | local dx, dy = x2-x1, y2-y1 86 | local nx, ny 87 | local nx1, ny1, nx2, ny2 = 0,0,0,0 88 | local p, q, r 89 | 90 | for side = 1,4 do 91 | if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left 92 | elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right 93 | elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top 94 | else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom 95 | end 96 | 97 | if p == 0 then 98 | if q <= 0 then return nil end 99 | else 100 | r = q / p 101 | if p < 0 then 102 | if r > ti2 then return nil 103 | elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny 104 | end 105 | else -- p > 0 106 | if r < ti1 then return nil 107 | elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny 108 | end 109 | end 110 | end 111 | end 112 | 113 | return ti1,ti2, nx1,ny1, nx2,ny2 114 | end 115 | 116 | -- Calculates the minkowsky difference between 2 rects, which is another rect 117 | local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) 118 | return x2 - x1 - w1, 119 | y2 - y1 - h1, 120 | w1 + w2, 121 | h1 + h2 122 | end 123 | 124 | local function rect_containsPoint(x,y,w,h, px,py) 125 | return px - x > DELTA and py - y > DELTA and 126 | x + w - px > DELTA and y + h - py > DELTA 127 | end 128 | 129 | local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2) 130 | return x1 < x2+w2 and x2 < x1+w1 and 131 | y1 < y2+h2 and y2 < y1+h1 132 | end 133 | 134 | local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2) 135 | local dx = x1 - x2 + (w1 - w2)/2 136 | local dy = y1 - y2 + (h1 - h2)/2 137 | return dx*dx + dy*dy 138 | end 139 | 140 | local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY) 141 | goalX = goalX or x1 142 | goalY = goalY or y1 143 | 144 | local dx, dy = goalX - x1, goalY - y1 145 | local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) 146 | 147 | local overlaps, ti, nx, ny 148 | 149 | if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other 150 | local px, py = rect_getNearestCorner(x,y,w,h, 0, 0) 151 | local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection 152 | ti = -wi * hi -- ti is the negative area of intersection 153 | overlaps = true 154 | else 155 | local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge) 156 | 157 | -- item tunnels into other 158 | if ti1 159 | and ti1 < 1 160 | and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner 161 | and (0 < ti1 + DELTA 162 | or 0 == ti1 and ti2 > 0) 163 | then 164 | ti, nx, ny = ti1, nx1, ny1 165 | overlaps = false 166 | end 167 | end 168 | 169 | if not ti then return end 170 | 171 | local tx, ty 172 | 173 | if overlaps then 174 | if dx == 0 and dy == 0 then 175 | -- intersecting and not moving - use minimum displacement vector 176 | local px, py = rect_getNearestCorner(x,y,w,h, 0,0) 177 | if abs(px) < abs(py) then py = 0 else px = 0 end 178 | nx, ny = sign(px), sign(py) 179 | tx, ty = x1 + px, y1 + py 180 | else 181 | -- intersecting and moving - move in the opposite direction 182 | local ti1, _ 183 | ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1) 184 | if not ti1 then return end 185 | tx, ty = x1 + dx * ti1, y1 + dy * ti1 186 | end 187 | else -- tunnel 188 | tx, ty = x1 + dx * ti, y1 + dy * ti 189 | end 190 | 191 | return { 192 | overlaps = overlaps, 193 | ti = ti, 194 | move = {x = dx, y = dy}, 195 | normal = {x = nx, y = ny}, 196 | touch = {x = tx, y = ty}, 197 | itemRect = {x = x1, y = y1, w = w1, h = h1}, 198 | otherRect = {x = x2, y = y2, w = w2, h = h2} 199 | } 200 | end 201 | 202 | ------------------------------------------ 203 | -- Grid functions 204 | ------------------------------------------ 205 | 206 | local function grid_toWorld(cellSize, cx, cy) 207 | return (cx - 1)*cellSize, (cy-1)*cellSize 208 | end 209 | 210 | local function grid_toCell(cellSize, x, y) 211 | return floor(x / cellSize) + 1, floor(y / cellSize) + 1 212 | end 213 | 214 | -- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing", 215 | -- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf 216 | -- It has been modified to include both cells when the ray "touches a grid corner", 217 | -- and with a different exit condition 218 | 219 | local function grid_traverse_initStep(cellSize, ct, t1, t2) 220 | local v = t2 - t1 221 | if v > 0 then 222 | return 1, cellSize / v, ((ct + v) * cellSize - t1) / v 223 | elseif v < 0 then 224 | return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v 225 | else 226 | return 0, math.huge, math.huge 227 | end 228 | end 229 | 230 | local function grid_traverse(cellSize, x1,y1,x2,y2, f) 231 | local cx1,cy1 = grid_toCell(cellSize, x1,y1) 232 | local cx2,cy2 = grid_toCell(cellSize, x2,y2) 233 | local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2) 234 | local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2) 235 | local cx,cy = cx1,cy1 236 | 237 | f(cx, cy) 238 | 239 | -- The default implementation had an infinite loop problem when 240 | -- approaching the last cell in some occassions. We finish iterating 241 | -- when we are *next* to the last cell 242 | while abs(cx - cx2) + abs(cy - cy2) > 1 do 243 | if tx < ty then 244 | tx, cx = tx + dx, cx + stepX 245 | f(cx, cy) 246 | else 247 | -- Addition: include both cells when going through corners 248 | if tx == ty then f(cx + stepX, cy) end 249 | ty, cy = ty + dy, cy + stepY 250 | f(cx, cy) 251 | end 252 | end 253 | 254 | -- If we have not arrived to the last cell, use it 255 | if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end 256 | 257 | end 258 | 259 | local function grid_toCellRect(cellSize, x,y,w,h) 260 | local cx,cy = grid_toCell(cellSize, x, y) 261 | local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize) 262 | return cx, cy, cr - cx + 1, cb - cy + 1 263 | end 264 | 265 | ------------------------------------------ 266 | -- Responses 267 | ------------------------------------------ 268 | 269 | local touch = function(world, col, x,y,w,h, goalX, goalY, filter) 270 | return col.touch.x, col.touch.y, {}, 0 271 | end 272 | 273 | local cross = function(world, col, x,y,w,h, goalX, goalY, filter) 274 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 275 | return goalX, goalY, cols, len 276 | end 277 | 278 | local slide = function(world, col, x,y,w,h, goalX, goalY, filter) 279 | goalX = goalX or x 280 | goalY = goalY or y 281 | 282 | local tch, move = col.touch, col.move 283 | if move.x ~= 0 or move.y ~= 0 then 284 | if col.normal.x ~= 0 then 285 | goalX = tch.x 286 | else 287 | goalY = tch.y 288 | end 289 | end 290 | 291 | col.slide = {x = goalX, y = goalY} 292 | 293 | x,y = tch.x, tch.y 294 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 295 | return goalX, goalY, cols, len 296 | end 297 | 298 | local bounce = function(world, col, x,y,w,h, goalX, goalY, filter) 299 | goalX = goalX or x 300 | goalY = goalY or y 301 | 302 | local tch, move = col.touch, col.move 303 | local tx, ty = tch.x, tch.y 304 | 305 | local bx, by = tx, ty 306 | 307 | if move.x ~= 0 or move.y ~= 0 then 308 | local bnx, bny = goalX - tx, goalY - ty 309 | if col.normal.x == 0 then bny = -bny else bnx = -bnx end 310 | bx, by = tx + bnx, ty + bny 311 | end 312 | 313 | col.bounce = {x = bx, y = by} 314 | x,y = tch.x, tch.y 315 | goalX, goalY = bx, by 316 | 317 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 318 | return goalX, goalY, cols, len 319 | end 320 | 321 | local onewayplatform = function(world, col, x,y,w,h, goalX, goalY, filter) 322 | if col.normal.y < 0 and not col.overlaps then 323 | col.didTouch = true 324 | goalX, goalY, cols, len = slide(world, col, x,y,w,h, goalX, goalY, filter) 325 | return goalX, goalY, cols, len 326 | else 327 | goalX, goalY, cols, len = cross(world, col, x,y,w,h, goalX, goalY, filter) 328 | return goalX, goalY, cols, len 329 | end 330 | end 331 | 332 | local onewayplatformTouch = function(world, col, x,y,w,h, goalX, goalY, filter) 333 | if col.normal.y < 0 and not col.overlaps then 334 | col.didTouch = true 335 | goalX, goalY, cols, len = touch(world, col, x,y,w,h, goalX, goalY, filter) 336 | return goalX, goalY, cols, len 337 | else 338 | goalX, goalY, cols, len = cross(world, col, x,y,w,h, goalX, goalY, filter) 339 | return goalX, goalY, cols, len 340 | end 341 | end 342 | 343 | ------------------------------------------ 344 | -- World 345 | ------------------------------------------ 346 | 347 | local World = {} 348 | local World_mt = {__index = World} 349 | 350 | -- Private functions and methods 351 | 352 | local function sortByWeight(a,b) return a.weight < b.weight end 353 | 354 | local function sortByTiAndDistance(a,b) 355 | if a.ti == b.ti then 356 | local ir, ar, br = a.itemRect, a.otherRect, b.otherRect 357 | local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h) 358 | local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h) 359 | return ad < bd 360 | end 361 | return a.ti < b.ti 362 | end 363 | 364 | local function addItemToCell(self, item, cx, cy) 365 | self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'}) 366 | local row = self.rows[cy] 367 | row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})} 368 | local cell = row[cx] 369 | self.nonEmptyCells[cell] = true 370 | if not cell.items[item] then 371 | cell.items[item] = true 372 | cell.itemCount = cell.itemCount + 1 373 | end 374 | end 375 | 376 | local function removeItemFromCell(self, item, cx, cy) 377 | local row = self.rows[cy] 378 | if not row or not row[cx] or not row[cx].items[item] then return false end 379 | 380 | local cell = row[cx] 381 | cell.items[item] = nil 382 | cell.itemCount = cell.itemCount - 1 383 | if cell.itemCount == 0 then 384 | self.nonEmptyCells[cell] = nil 385 | end 386 | return true 387 | end 388 | 389 | local function getDictItemsInCellRect(self, cl,ct,cw,ch) 390 | local items_dict = {} 391 | for cy=ct,ct+ch-1 do 392 | local row = self.rows[cy] 393 | if row then 394 | for cx=cl,cl+cw-1 do 395 | local cell = row[cx] 396 | if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling 397 | for item,_ in pairs(cell.items) do 398 | items_dict[item] = true 399 | end 400 | end 401 | end 402 | end 403 | end 404 | 405 | return items_dict 406 | end 407 | 408 | local function getCellsTouchedBySegment(self, x1,y1,x2,y2) 409 | 410 | local cells, cellsLen, visited = {}, 0, {} 411 | 412 | grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy) 413 | local row = self.rows[cy] 414 | if not row then return end 415 | local cell = row[cx] 416 | if not cell or visited[cell] then return end 417 | 418 | visited[cell] = true 419 | cellsLen = cellsLen + 1 420 | cells[cellsLen] = cell 421 | end) 422 | 423 | return cells, cellsLen 424 | end 425 | 426 | local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter) 427 | local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2) 428 | local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1 429 | local visited, itemInfo, itemInfoLen = {},{},0 430 | for i=1,len do 431 | cell = cells[i] 432 | for item in pairs(cell.items) do 433 | if not visited[item] then 434 | visited[item] = true 435 | if (not filter or filter(item)) then 436 | rect = self.rects[item] 437 | l,t,w,h = rect.x,rect.y,rect.w,rect.h 438 | 439 | ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1) 440 | if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then 441 | -- the sorting is according to the t of an infinite line, not the segment 442 | tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge) 443 | itemInfoLen = itemInfoLen + 1 444 | itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)} 445 | end 446 | end 447 | end 448 | end 449 | end 450 | table.sort(itemInfo, sortByWeight) 451 | return itemInfo, itemInfoLen 452 | end 453 | 454 | local function getResponseByName(self, name) 455 | local response = self.responses[name] 456 | if not response then 457 | error(('Unknown collision type: %s (%s)'):format(name, type(name))) 458 | end 459 | return response 460 | end 461 | 462 | 463 | -- Misc Public Methods 464 | 465 | function World:addResponse(name, response) 466 | self.responses[name] = response 467 | end 468 | 469 | function World:project(item, x,y,w,h, goalX, goalY, filter) 470 | assertIsRect(x,y,w,h) 471 | 472 | goalX = goalX or x 473 | goalY = goalY or y 474 | filter = filter or defaultFilter 475 | 476 | local collisions, len = {}, 0 477 | 478 | local visited = {} 479 | if item ~= nil then visited[item] = true end 480 | 481 | -- This could probably be done with less cells using a polygon raster over the cells instead of a 482 | -- bounding rect of the whole movement. Conditional to building a queryPolygon method 483 | local tl, tt = min(goalX, x), min(goalY, y) 484 | local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h) 485 | local tw, th = tr-tl, tb-tt 486 | 487 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th) 488 | 489 | local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) 490 | 491 | for other,_ in pairs(dictItemsInCellRect) do 492 | if not visited[other] then 493 | visited[other] = true 494 | 495 | local responseName = filter(item, other) 496 | if responseName then 497 | local ox,oy,ow,oh = self:getRect(other) 498 | local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY) 499 | 500 | if col then 501 | col.other = other 502 | col.item = item 503 | col.type = responseName 504 | 505 | len = len + 1 506 | collisions[len] = col 507 | end 508 | end 509 | end 510 | end 511 | 512 | table.sort(collisions, sortByTiAndDistance) 513 | 514 | return collisions, len 515 | end 516 | 517 | function World:countCells() 518 | local count = 0 519 | for _,row in pairs(self.rows) do 520 | for _,_ in pairs(row) do 521 | count = count + 1 522 | end 523 | end 524 | return count 525 | end 526 | 527 | function World:hasItem(item) 528 | return not not self.rects[item] 529 | end 530 | 531 | function World:getItems() 532 | local items, len = {}, 0 533 | for item,_ in pairs(self.rects) do 534 | len = len + 1 535 | items[len] = item 536 | end 537 | return items, len 538 | end 539 | 540 | function World:countItems() 541 | local len = 0 542 | for _ in pairs(self.rects) do len = len + 1 end 543 | return len 544 | end 545 | 546 | function World:getRect(item) 547 | local rect = self.rects[item] 548 | if not rect then 549 | error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.') 550 | end 551 | return rect.x, rect.y, rect.w, rect.h 552 | end 553 | 554 | function World:toWorld(cx, cy) 555 | return grid_toWorld(self.cellSize, cx, cy) 556 | end 557 | 558 | function World:toCell(x,y) 559 | return grid_toCell(self.cellSize, x, y) 560 | end 561 | 562 | 563 | --- Query methods 564 | 565 | function World:queryRect(x,y,w,h, filter) 566 | 567 | assertIsRect(x,y,w,h) 568 | 569 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 570 | local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) 571 | 572 | local items, len = {}, 0 573 | 574 | local rect 575 | for item,_ in pairs(dictItemsInCellRect) do 576 | rect = self.rects[item] 577 | if (not filter or filter(item)) 578 | and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h) 579 | then 580 | len = len + 1 581 | items[len] = item 582 | end 583 | end 584 | 585 | return items, len 586 | end 587 | 588 | function World:queryPoint(x,y, filter) 589 | local cx,cy = self:toCell(x,y) 590 | local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1) 591 | 592 | local items, len = {}, 0 593 | 594 | local rect 595 | for item,_ in pairs(dictItemsInCellRect) do 596 | rect = self.rects[item] 597 | if (not filter or filter(item)) 598 | and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y) 599 | then 600 | len = len + 1 601 | items[len] = item 602 | end 603 | end 604 | 605 | return items, len 606 | end 607 | 608 | function World:querySegment(x1, y1, x2, y2, filter) 609 | local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) 610 | local items = {} 611 | for i=1, len do 612 | items[i] = itemInfo[i].item 613 | end 614 | return items, len 615 | end 616 | 617 | function World:querySegmentWithCoords(x1, y1, x2, y2, filter) 618 | local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) 619 | local dx, dy = x2-x1, y2-y1 620 | local info, ti1, ti2 621 | for i=1, len do 622 | info = itemInfo[i] 623 | ti1 = info.ti1 624 | ti2 = info.ti2 625 | 626 | info.weight = nil 627 | info.x1 = x1 + dx * ti1 628 | info.y1 = y1 + dy * ti1 629 | info.x2 = x1 + dx * ti2 630 | info.y2 = y1 + dy * ti2 631 | end 632 | return itemInfo, len 633 | end 634 | 635 | 636 | --- Main methods 637 | 638 | function World:add(item, x,y,w,h) 639 | local rect = self.rects[item] 640 | if rect then 641 | error('Item ' .. tostring(item) .. ' added to the world twice.') 642 | end 643 | assertIsRect(x,y,w,h) 644 | 645 | self.rects[item] = {x=x,y=y,w=w,h=h} 646 | 647 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 648 | for cy = ct, ct+ch-1 do 649 | for cx = cl, cl+cw-1 do 650 | addItemToCell(self, item, cx, cy) 651 | end 652 | end 653 | 654 | return item 655 | end 656 | 657 | function World:remove(item) 658 | local x,y,w,h = self:getRect(item) 659 | 660 | self.rects[item] = nil 661 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 662 | for cy = ct, ct+ch-1 do 663 | for cx = cl, cl+cw-1 do 664 | removeItemFromCell(self, item, cx, cy) 665 | end 666 | end 667 | end 668 | 669 | function World:update(item, x2,y2,w2,h2) 670 | local x1,y1,w1,h1 = self:getRect(item) 671 | w2,h2 = w2 or w1, h2 or h1 672 | assertIsRect(x2,y2,w2,h2) 673 | 674 | if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then 675 | 676 | local cellSize = self.cellSize 677 | local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1) 678 | local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2) 679 | 680 | if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then 681 | 682 | local cr1, cb1 = cl1+cw1-1, ct1+ch1-1 683 | local cr2, cb2 = cl2+cw2-1, ct2+ch2-1 684 | local cyOut 685 | 686 | for cy = ct1, cb1 do 687 | cyOut = cy < ct2 or cy > cb2 688 | for cx = cl1, cr1 do 689 | if cyOut or cx < cl2 or cx > cr2 then 690 | removeItemFromCell(self, item, cx, cy) 691 | end 692 | end 693 | end 694 | 695 | for cy = ct2, cb2 do 696 | cyOut = cy < ct1 or cy > cb1 697 | for cx = cl2, cr2 do 698 | if cyOut or cx < cl1 or cx > cr1 then 699 | addItemToCell(self, item, cx, cy) 700 | end 701 | end 702 | end 703 | 704 | end 705 | 706 | local rect = self.rects[item] 707 | rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2 708 | 709 | end 710 | end 711 | 712 | function World:move(item, goalX, goalY, filter) 713 | local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter) 714 | 715 | self:update(item, actualX, actualY) 716 | 717 | return actualX, actualY, cols, len 718 | end 719 | 720 | function World:check(item, goalX, goalY, filter) 721 | filter = filter or defaultFilter 722 | 723 | local visited = {[item] = true} 724 | local visitedFilter = function(itm, other) 725 | if visited[other] then return false end 726 | return filter(itm, other) 727 | end 728 | 729 | local cols, len = {}, 0 730 | 731 | local x,y,w,h = self:getRect(item) 732 | 733 | local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter) 734 | 735 | while projected_len > 0 do 736 | local col = projected_cols[1] 737 | len = len + 1 738 | cols[len] = col 739 | 740 | visited[col.other] = true 741 | 742 | local response = getResponseByName(self, col.type) 743 | 744 | goalX, goalY, projected_cols, projected_len = response( 745 | self, 746 | col, 747 | x, y, w, h, 748 | goalX, goalY, 749 | visitedFilter 750 | ) 751 | end 752 | 753 | return goalX, goalY, cols, len 754 | end 755 | 756 | 757 | -- Public library functions 758 | 759 | bump.newWorld = function(cellSize) 760 | cellSize = cellSize or 64 761 | assertIsPositiveNumber(cellSize, 'cellSize') 762 | local world = setmetatable({ 763 | cellSize = cellSize, 764 | rects = {}, 765 | rows = {}, 766 | nonEmptyCells = {}, 767 | responses = {} 768 | }, World_mt) 769 | 770 | world:addResponse('touch', touch) 771 | world:addResponse('cross', cross) 772 | world:addResponse('slide', slide) 773 | world:addResponse('bounce', bounce) 774 | world:addResponse('onewayplatform', onewayplatform) 775 | world:addResponse('onewayplatformTouch', onewayplatformTouch) 776 | 777 | return world 778 | end 779 | 780 | bump.rect = { 781 | getNearestCorner = rect_getNearestCorner, 782 | getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices, 783 | getDiff = rect_getDiff, 784 | containsPoint = rect_containsPoint, 785 | isIntersecting = rect_isIntersecting, 786 | getSquareDistance = rect_getSquareDistance, 787 | detectCollision = rect_detectCollision 788 | } 789 | 790 | bump.responses = { 791 | touch = touch, 792 | cross = cross, 793 | slide = slide, 794 | bounce = bounce 795 | } 796 | 797 | return bump 798 | -------------------------------------------------------------------------------- /lib/tiny.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2016 Calvin Rose 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | ]] 21 | 22 | --- @module tiny-ecs 23 | -- @author Calvin Rose 24 | -- @license MIT 25 | -- @copyright 2016 26 | local tiny = {} 27 | 28 | -- Local versions of standard lua functions 29 | local tinsert = table.insert 30 | local tremove = table.remove 31 | local tsort = table.sort 32 | local setmetatable = setmetatable 33 | local type = type 34 | local select = select 35 | 36 | -- Local versions of the library functions 37 | local tiny_manageEntities 38 | local tiny_manageSystems 39 | local tiny_addEntity 40 | local tiny_addSystem 41 | local tiny_add 42 | local tiny_removeEntity 43 | local tiny_removeSystem 44 | 45 | --- Filter functions. 46 | -- A Filter is a function that selects which Entities apply to a System. 47 | -- Filters take two parameters, the System and the Entity, and return a boolean 48 | -- value indicating if the Entity should be processed by the System. A truthy 49 | -- value includes the entity, while a falsey (nil or false) value excludes the 50 | -- entity. 51 | -- 52 | -- Filters must be added to Systems by setting the `filter` field of the System. 53 | -- Filter's returned by tiny-ecs's Filter functions are immutable and can be 54 | -- used by multiple Systems. 55 | -- 56 | -- local f1 = tiny.requireAll("position", "velocity", "size") 57 | -- local f2 = tiny.requireAny("position", "velocity", "size") 58 | -- 59 | -- local e1 = { 60 | -- position = {2, 3}, 61 | -- velocity = {3, 3}, 62 | -- size = {4, 4} 63 | -- } 64 | -- 65 | -- local entity2 = { 66 | -- position = {4, 5}, 67 | -- size = {4, 4} 68 | -- } 69 | -- 70 | -- local e3 = { 71 | -- position = {2, 3}, 72 | -- velocity = {3, 3} 73 | -- } 74 | -- 75 | -- print(f1(nil, e1), f1(nil, e2), f1(nil, e3)) -- prints true, false, false 76 | -- print(f2(nil, e1), f2(nil, e2), f2(nil, e3)) -- prints true, true, true 77 | -- 78 | -- Filters can also be passed as arguments to other Filter constructors. This is 79 | -- a powerful way to create complex, custom Filters that select a very specific 80 | -- set of Entities. 81 | -- 82 | -- -- Selects Entities with an "image" Component, but not Entities with a 83 | -- -- "Player" or "Enemy" Component. 84 | -- filter = tiny.requireAll("image", tiny.rejectAny("Player", "Enemy")) 85 | -- 86 | -- @section Filter 87 | 88 | -- A helper function to compile filters. 89 | local filterJoin 90 | 91 | -- A helper function to filters from string 92 | local filterBuildString 93 | 94 | do 95 | 96 | local loadstring = loadstring or load 97 | local function getchr(c) 98 | return "\\" .. c:byte() 99 | end 100 | local function make_safe(text) 101 | return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr) 102 | end 103 | 104 | local function filterJoinRaw(prefix, seperator, ...) 105 | local accum = {} 106 | local build = {} 107 | for i = 1, select('#', ...) do 108 | local item = select(i, ...) 109 | if type(item) == 'string' then 110 | accum[#accum + 1] = ("(e[%s] ~= nil)"):format(make_safe(item)) 111 | elseif type(item) == 'function' then 112 | build[#build + 1] = ('local subfilter_%d_ = select(%d, ...)') 113 | :format(i, i) 114 | accum[#accum + 1] = ('(subfilter_%d_(system, e))'):format(i) 115 | else 116 | error 'Filter token must be a string or a filter function.' 117 | end 118 | end 119 | local source = ('%s\nreturn function(system, e) return %s(%s) end') 120 | :format( 121 | table.concat(build, '\n'), 122 | prefix, 123 | table.concat(accum, seperator)) 124 | local loader, err = loadstring(source) 125 | if err then error(err) end 126 | return loader(...) 127 | end 128 | 129 | function filterJoin(...) 130 | local state, value = pcall(filterJoinRaw, ...) 131 | if state then return value else return nil, value end 132 | end 133 | 134 | local function buildPart(str) 135 | local accum = {} 136 | local subParts = {} 137 | str = str:gsub('%b()', function(p) 138 | subParts[#subParts + 1] = buildPart(p:sub(2, -2)) 139 | return ('\255%d'):format(#subParts) 140 | end) 141 | for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do 142 | if part:match('^\255%d+$') then 143 | local partIndex = tonumber(part:match(part:sub(2))) 144 | accum[#accum + 1] = ('%s(%s)') 145 | :format(invert == '' and '' or 'not', subParts[partIndex]) 146 | else 147 | accum[#accum + 1] = ("(e[%s] %s nil)") 148 | :format(make_safe(part), invert == '' and '~=' or '==') 149 | end 150 | if sep ~= '' then 151 | accum[#accum + 1] = (sep == '|' and ' or ' or ' and ') 152 | end 153 | end 154 | return table.concat(accum) 155 | end 156 | 157 | function filterBuildString(str) 158 | local source = ("return function(_, e) return %s end") 159 | :format(buildPart(str)) 160 | local loader, err = loadstring(source) 161 | if err then 162 | error(err) 163 | end 164 | return loader() 165 | end 166 | 167 | end 168 | 169 | --- Makes a Filter that selects Entities with all specified Components and 170 | -- Filters. 171 | function tiny.requireAll(...) 172 | return filterJoin('', ' and ', ...) 173 | end 174 | 175 | --- Makes a Filter that selects Entities with at least one of the specified 176 | -- Components and Filters. 177 | function tiny.requireAny(...) 178 | return filterJoin('', ' or ', ...) 179 | end 180 | 181 | --- Makes a Filter that rejects Entities with all specified Components and 182 | -- Filters, and selects all other Entities. 183 | function tiny.rejectAll(...) 184 | return filterJoin('not', ' and ', ...) 185 | end 186 | 187 | --- Makes a Filter that rejects Entities with at least one of the specified 188 | -- Components and Filters, and selects all other Entities. 189 | function tiny.rejectAny(...) 190 | return filterJoin('not', ' or ', ...) 191 | end 192 | 193 | --- Makes a Filter from a string. Syntax of `pattern` is as follows. 194 | -- 195 | -- * Tokens are alphanumeric strings including underscores. 196 | -- * Tokens can be separated by |, &, or surrounded by parentheses. 197 | -- * Tokens can be prefixed with !, and are then inverted. 198 | -- 199 | -- Examples are best: 200 | -- 'a|b|c' - Matches entities with an 'a' OR 'b' OR 'c'. 201 | -- 'a&!b&c' - Matches entities with an 'a' AND NOT 'b' AND 'c'. 202 | -- 'a|(b&c&d)|e - Matches 'a' OR ('b' AND 'c' AND 'd') OR 'e' 203 | -- @param pattern 204 | function tiny.filter(pattern) 205 | local state, value = pcall(filterBuildString, pattern) 206 | if state then return value else return nil, value end 207 | end 208 | 209 | --- System functions. 210 | -- A System is a wrapper around function callbacks for manipulating Entities. 211 | -- Systems are implemented as tables that contain at least one method; 212 | -- an update function that takes parameters like so: 213 | -- 214 | -- * `function system:update(dt)`. 215 | -- 216 | -- There are also a few other optional callbacks: 217 | -- 218 | -- * `function system:filter(entity)` - Returns true if this System should 219 | -- include this Entity, otherwise should return false. If this isn't specified, 220 | -- no Entities are included in the System. 221 | -- * `function system:onAdd(entity)` - Called when an Entity is added to the 222 | -- System. 223 | -- * `function system:onRemove(entity)` - Called when an Entity is removed 224 | -- from the System. 225 | -- * `function system:onModify(dt)` - Called when the System is modified by 226 | -- adding or removing Entities from the System. 227 | -- * `function system:onAddToWorld(world)` - Called when the System is added 228 | -- to the World, before any entities are added to the system. 229 | -- * `function system:onRemoveFromWorld(world)` - Called when the System is 230 | -- removed from the world, after all Entities are removed from the System. 231 | -- * `function system:preWrap(dt)` - Called on each system before update is 232 | -- called on any system. 233 | -- * `function system:postWrap(dt)` - Called on each system in reverse order 234 | -- after update is called on each system. The idea behind `preWrap` and 235 | -- `postWrap` is to allow for systems that modify the behavior of other systems. 236 | -- Say there is a DrawingSystem, which draws sprites to the screen, and a 237 | -- PostProcessingSystem, that adds some blur and bloom effects. In the preWrap 238 | -- method of the PostProcessingSystem, the System could set the drawing target 239 | -- for the DrawingSystem to a special buffer instead the screen. In the postWrap 240 | -- method, the PostProcessingSystem could then modify the buffer and render it 241 | -- to the screen. In this setup, the PostProcessingSystem would be added to the 242 | -- World after the drawingSystem (A similar but less flexible behavior could 243 | -- be accomplished with a single custom update function in the DrawingSystem). 244 | -- 245 | -- For Filters, it is convenient to use `tiny.requireAll` or `tiny.requireAny`, 246 | -- but one can write their own filters as well. Set the Filter of a System like 247 | -- so: 248 | -- system.filter = tiny.requireAll("a", "b", "c") 249 | -- or 250 | -- function system:filter(entity) 251 | -- return entity.myRequiredComponentName ~= nil 252 | -- end 253 | -- 254 | -- All Systems also have a few important fields that are initialized when the 255 | -- system is added to the World. A few are important, and few should be less 256 | -- commonly used. 257 | -- 258 | -- * The `world` field points to the World that the System belongs to. Useful 259 | -- for adding and removing Entities from the world dynamically via the System. 260 | -- * The `active` flag is whether or not the System is updated automatically. 261 | -- Inactive Systems should be updated manually or not at all via 262 | -- `system:update(dt)`. Defaults to true. 263 | -- * The `entities` field is an ordered list of Entities in the System. This 264 | -- list can be used to quickly iterate through all Entities in a System. 265 | -- * The `interval` field is an optional field that makes Systems update at 266 | -- certain intervals using buffered time, regardless of World update frequency. 267 | -- For example, to make a System update once a second, set the System's interval 268 | -- to 1. 269 | -- * The `index` field is the System's index in the World. Lower indexed 270 | -- Systems are processed before higher indices. The `index` is a read only 271 | -- field; to set the `index`, use `tiny.setSystemIndex(world, system)`. 272 | -- * The `indices` field is a table of Entity keys to their indices in the 273 | -- `entities` list. Most Systems can ignore this. 274 | -- * The `modified` flag is an indicator if the System has been modified in 275 | -- the last update. If so, the `onModify` callback will be called on the System 276 | -- in the next update, if it has one. This is usually managed by tiny-ecs, so 277 | -- users should mostly ignore this, too. 278 | -- 279 | -- There is another option to (hopefully) increase performance in systems that 280 | -- have items added to or removed from them often, and have lots of entities in 281 | -- them. Setting the `nocache` field of the system might improve performance. 282 | -- It is still experimental. There are some restriction to systems without 283 | -- caching, however. 284 | -- 285 | -- * There is no `entities` table. 286 | -- * Callbacks such onAdd, onRemove, and onModify will never be called 287 | -- * Noncached systems cannot be sorted (There is no entities list to sort). 288 | -- 289 | -- @section System 290 | 291 | -- Use an empty table as a key for identifying Systems. Any table that contains 292 | -- this key is considered a System rather than an Entity. 293 | local systemTableKey = { "SYSTEM_TABLE_KEY" } 294 | 295 | -- Checks if a table is a System. 296 | local function isSystem(table) 297 | return table[systemTableKey] 298 | end 299 | 300 | -- Update function for all Processing Systems. 301 | local function processingSystemUpdate(system, dt) 302 | local preProcess = system.preProcess 303 | local process = system.process 304 | local postProcess = system.postProcess 305 | 306 | if preProcess then 307 | preProcess(system, dt) 308 | end 309 | 310 | if process then 311 | if system.nocache then 312 | local entities = system.world.entityList 313 | local filter = system.filter 314 | if filter then 315 | for i = 1, #entities do 316 | local entity = entities[i] 317 | if filter(system, entity) then 318 | process(system, entity, dt) 319 | end 320 | end 321 | end 322 | else 323 | local entities = system.entities 324 | for i = 1, #entities do 325 | process(system, entities[i], dt) 326 | end 327 | end 328 | end 329 | 330 | if postProcess then 331 | postProcess(system, dt) 332 | end 333 | end 334 | 335 | -- Sorts Systems by a function system.sortDelegate(entity1, entity2) on modify. 336 | local function sortedSystemOnModify(system) 337 | local entities = system.entities 338 | local indices = system.indices 339 | local sortDelegate = system.sortDelegate 340 | if not sortDelegate then 341 | local compare = system.compare 342 | sortDelegate = function(e1, e2) 343 | return compare(system, e1, e2) 344 | end 345 | system.sortDelegate = sortDelegate 346 | end 347 | tsort(entities, sortDelegate) 348 | for i = 1, #entities do 349 | indices[entities[i]] = i 350 | end 351 | end 352 | 353 | --- Creates a new System or System class from the supplied table. If `table` is 354 | -- nil, creates a new table. 355 | function tiny.system(table) 356 | table = table or {} 357 | table[systemTableKey] = true 358 | return table 359 | end 360 | 361 | --- Creates a new Processing System or Processing System class. Processing 362 | -- Systems process each entity individual, and are usually what is needed. 363 | -- Processing Systems have three extra callbacks besides those inheritted from 364 | -- vanilla Systems. 365 | -- 366 | -- function system:preProcess(dt) -- Called before iteration. 367 | -- function system:process(entity, dt) -- Process each entity. 368 | -- function system:postProcess(dt) -- Called after iteration. 369 | -- 370 | -- Processing Systems have their own `update` method, so don't implement a 371 | -- a custom `update` callback for Processing Systems. 372 | -- @see system 373 | function tiny.processingSystem(table) 374 | table = table or {} 375 | table[systemTableKey] = true 376 | table.update = processingSystemUpdate 377 | return table 378 | end 379 | 380 | --- Creates a new Sorted System or Sorted System class. Sorted Systems sort 381 | -- their Entities according to a user-defined method, `system:compare(e1, e2)`, 382 | -- which should return true if `e1` should come before `e2` and false otherwise. 383 | -- Sorted Systems also override the default System's `onModify` callback, so be 384 | -- careful if defining a custom callback. However, for processing the sorted 385 | -- entities, consider `tiny.sortedProcessingSystem(table)`. 386 | -- @see system 387 | function tiny.sortedSystem(table) 388 | table = table or {} 389 | table[systemTableKey] = true 390 | table.onModify = sortedSystemOnModify 391 | return table 392 | end 393 | 394 | --- Creates a new Sorted Processing System or Sorted Processing System class. 395 | -- Sorted Processing Systems have both the aspects of Processing Systems and 396 | -- Sorted Systems. 397 | -- @see system 398 | -- @see processingSystem 399 | -- @see sortedSystem 400 | function tiny.sortedProcessingSystem(table) 401 | table = table or {} 402 | table[systemTableKey] = true 403 | table.update = processingSystemUpdate 404 | table.onModify = sortedSystemOnModify 405 | return table 406 | end 407 | 408 | --- World functions. 409 | -- A World is a container that manages Entities and Systems. Typically, a 410 | -- program uses one World at a time. 411 | -- 412 | -- For all World functions except `tiny.world(...)`, object-oriented syntax can 413 | -- be used instead of the documented syntax. For example, 414 | -- `tiny.add(world, e1, e2, e3)` is the same as `world:add(e1, e2, e3)`. 415 | -- @section World 416 | 417 | -- Forward declaration 418 | local worldMetaTable 419 | 420 | --- Creates a new World. 421 | -- Can optionally add default Systems and Entities. Returns the new World along 422 | -- with default Entities and Systems. 423 | function tiny.world(...) 424 | local ret = setmetatable({ 425 | 426 | -- List of Entities to remove 427 | entitiesToRemove = {}, 428 | 429 | -- List of Entities to change 430 | entitiesToChange = {}, 431 | 432 | -- List of Entities to add 433 | systemsToAdd = {}, 434 | 435 | -- List of Entities to remove 436 | systemsToRemove = {}, 437 | 438 | -- Set of Entities 439 | entities = {}, 440 | 441 | -- List of Systems 442 | systems = {} 443 | 444 | }, worldMetaTable) 445 | 446 | tiny_add(ret, ...) 447 | tiny_manageSystems(ret) 448 | tiny_manageEntities(ret) 449 | 450 | return ret, ... 451 | end 452 | 453 | --- Adds an Entity to the world. 454 | -- Also call this on Entities that have changed Components such that they 455 | -- match different Filters. Returns the Entity. 456 | function tiny.addEntity(world, entity) 457 | local e2c = world.entitiesToChange 458 | e2c[#e2c + 1] = entity 459 | return entity 460 | end 461 | tiny_addEntity = tiny.addEntity 462 | 463 | --- Adds a System to the world. Returns the System. 464 | function tiny.addSystem(world, system) 465 | assert(system.world == nil, "System already belongs to a World.") 466 | local s2a = world.systemsToAdd 467 | s2a[#s2a + 1] = system 468 | system.world = world 469 | return system 470 | end 471 | tiny_addSystem = tiny.addSystem 472 | 473 | --- Shortcut for adding multiple Entities and Systems to the World. Returns all 474 | -- added Entities and Systems. 475 | function tiny.add(world, ...) 476 | for i = 1, select("#", ...) do 477 | local obj = select(i, ...) 478 | if obj then 479 | if isSystem(obj) then 480 | tiny_addSystem(world, obj) 481 | else -- Assume obj is an Entity 482 | tiny_addEntity(world, obj) 483 | end 484 | end 485 | end 486 | return ... 487 | end 488 | tiny_add = tiny.add 489 | 490 | --- Removes an Entity from the World. Returns the Entity. 491 | function tiny.removeEntity(world, entity) 492 | local e2r = world.entitiesToRemove 493 | e2r[#e2r + 1] = entity 494 | return entity 495 | end 496 | tiny_removeEntity = tiny.removeEntity 497 | 498 | --- Removes a System from the world. Returns the System. 499 | function tiny.removeSystem(world, system) 500 | assert(system.world == world, "System does not belong to this World.") 501 | local s2r = world.systemsToRemove 502 | s2r[#s2r + 1] = system 503 | return system 504 | end 505 | tiny_removeSystem = tiny.removeSystem 506 | 507 | --- Shortcut for removing multiple Entities and Systems from the World. Returns 508 | -- all removed Systems and Entities 509 | function tiny.remove(world, ...) 510 | for i = 1, select("#", ...) do 511 | local obj = select(i, ...) 512 | if obj then 513 | if isSystem(obj) then 514 | tiny_removeSystem(world, obj) 515 | else -- Assume obj is an Entity 516 | tiny_removeEntity(world, obj) 517 | end 518 | end 519 | end 520 | return ... 521 | end 522 | 523 | -- Adds and removes Systems that have been marked from the World. 524 | function tiny_manageSystems(world) 525 | local s2a, s2r = world.systemsToAdd, world.systemsToRemove 526 | 527 | -- Early exit 528 | if #s2a == 0 and #s2r == 0 then 529 | return 530 | end 531 | 532 | world.systemsToAdd = {} 533 | world.systemsToRemove = {} 534 | 535 | local worldEntityList = world.entities 536 | local systems = world.systems 537 | 538 | -- Remove Systems 539 | for i = 1, #s2r do 540 | local system = s2r[i] 541 | local index = system.index 542 | local onRemove = system.onRemove 543 | if onRemove and not system.nocache then 544 | local entityList = system.entities 545 | for j = 1, #entityList do 546 | onRemove(system, entityList[j]) 547 | end 548 | end 549 | tremove(systems, index) 550 | for j = index, #systems do 551 | systems[j].index = j 552 | end 553 | local onRemoveFromWorld = system.onRemoveFromWorld 554 | if onRemoveFromWorld then 555 | onRemoveFromWorld(system, world) 556 | end 557 | s2r[i] = nil 558 | 559 | -- Clean up System 560 | system.world = nil 561 | system.entities = nil 562 | system.indices = nil 563 | system.index = nil 564 | end 565 | 566 | -- Add Systems 567 | for i = 1, #s2a do 568 | local system = s2a[i] 569 | if systems[system.index or 0] ~= system then 570 | if not system.nocache then 571 | system.entities = {} 572 | system.indices = {} 573 | end 574 | if system.active == nil then 575 | system.active = true 576 | end 577 | system.modified = true 578 | system.world = world 579 | local index = #systems + 1 580 | system.index = index 581 | systems[index] = system 582 | local onAddToWorld = system.onAddToWorld 583 | if onAddToWorld then 584 | onAddToWorld(system, world) 585 | end 586 | 587 | -- Try to add Entities 588 | if not system.nocache then 589 | local entityList = system.entities 590 | local entityIndices = system.indices 591 | local onAdd = system.onAdd 592 | local filter = system.filter 593 | if filter then 594 | for j = 1, #worldEntityList do 595 | local entity = worldEntityList[j] 596 | if filter(system, entity) then 597 | local entityIndex = #entityList + 1 598 | entityList[entityIndex] = entity 599 | entityIndices[entity] = entityIndex 600 | if onAdd then 601 | onAdd(system, entity) 602 | end 603 | end 604 | end 605 | end 606 | end 607 | end 608 | s2a[i] = nil 609 | end 610 | end 611 | 612 | -- Adds, removes, and changes Entities that have been marked. 613 | function tiny_manageEntities(world) 614 | 615 | local e2r = world.entitiesToRemove 616 | local e2c = world.entitiesToChange 617 | 618 | -- Early exit 619 | if #e2r == 0 and #e2c == 0 then 620 | return 621 | end 622 | 623 | world.entitiesToChange = {} 624 | world.entitiesToRemove = {} 625 | 626 | local entities = world.entities 627 | local systems = world.systems 628 | 629 | -- Change Entities 630 | for i = 1, #e2c do 631 | local entity = e2c[i] 632 | -- Add if needed 633 | if not entities[entity] then 634 | local index = #entities + 1 635 | entities[entity] = index 636 | entities[index] = entity 637 | end 638 | for j = 1, #systems do 639 | local system = systems[j] 640 | if not system.nocache then 641 | local ses = system.entities 642 | local seis = system.indices 643 | local index = seis[entity] 644 | local filter = system.filter 645 | if filter and filter(system, entity) then 646 | if not index then 647 | system.modified = true 648 | index = #ses + 1 649 | ses[index] = entity 650 | seis[entity] = index 651 | local onAdd = system.onAdd 652 | if onAdd then 653 | onAdd(system, entity) 654 | end 655 | end 656 | elseif index then 657 | system.modified = true 658 | local tmpEntity = ses[#ses] 659 | ses[index] = tmpEntity 660 | seis[tmpEntity] = index 661 | seis[entity] = nil 662 | ses[#ses] = nil 663 | local onRemove = system.onRemove 664 | if onRemove then 665 | onRemove(system, entity) 666 | end 667 | end 668 | end 669 | end 670 | e2c[i] = nil 671 | end 672 | 673 | -- Remove Entities 674 | for i = 1, #e2r do 675 | local entity = e2r[i] 676 | e2r[i] = nil 677 | local listIndex = entities[entity] 678 | if listIndex then 679 | -- Remove Entity from world state 680 | local lastEntity = entities[#entities] 681 | entities[lastEntity] = listIndex 682 | entities[entity] = nil 683 | entities[listIndex] = lastEntity 684 | entities[#entities] = nil 685 | -- Remove from cached systems 686 | for j = 1, #systems do 687 | local system = systems[j] 688 | if not system.nocache then 689 | local ses = system.entities 690 | local seis = system.indices 691 | local index = seis[entity] 692 | if index then 693 | system.modified = true 694 | local tmpEntity = ses[#ses] 695 | ses[index] = tmpEntity 696 | seis[tmpEntity] = index 697 | seis[entity] = nil 698 | ses[#ses] = nil 699 | local onRemove = system.onRemove 700 | if onRemove then 701 | onRemove(system, entity) 702 | end 703 | end 704 | end 705 | end 706 | end 707 | end 708 | end 709 | 710 | --- Manages Entities and Systems marked for deletion or addition. Call this 711 | -- before modifying Systems and Entities outside of a call to `tiny.update`. 712 | -- Do not call this within a call to `tiny.update`. 713 | function tiny.refresh(world) 714 | tiny_manageSystems(world) 715 | tiny_manageEntities(world) 716 | local systems = world.systems 717 | for i = #systems, 1, -1 do 718 | local system = systems[i] 719 | if system.active then 720 | local onModify = system.onModify 721 | if onModify and system.modified then 722 | onModify(system, 0) 723 | end 724 | system.modified = false 725 | end 726 | end 727 | end 728 | 729 | --- Updates the World by dt (delta time). Takes an optional parameter, `filter`, 730 | -- which is a Filter that selects Systems from the World, and updates only those 731 | -- Systems. If `filter` is not supplied, all Systems are updated. Put this 732 | -- function in your main loop. 733 | function tiny.update(world, dt, filter) 734 | 735 | tiny_manageSystems(world) 736 | tiny_manageEntities(world) 737 | 738 | local systems = world.systems 739 | 740 | -- Iterate through Systems IN REVERSE ORDER 741 | for i = #systems, 1, -1 do 742 | local system = systems[i] 743 | if system.active then 744 | -- Call the modify callback on Systems that have been modified. 745 | local onModify = system.onModify 746 | if onModify and system.modified then 747 | onModify(system, dt) 748 | end 749 | local preWrap = system.preWrap 750 | if preWrap and 751 | ((not filter) or filter(world, system)) then 752 | preWrap(system, dt) 753 | end 754 | end 755 | end 756 | 757 | -- Iterate through Systems IN ORDER 758 | for i = 1, #systems do 759 | local system = systems[i] 760 | if system.active and ((not filter) or filter(world, system)) then 761 | 762 | -- Update Systems that have an update method (most Systems) 763 | local update = system.update 764 | if update then 765 | local interval = system.interval 766 | if interval then 767 | local bufferedTime = (system.bufferedTime or 0) + dt 768 | while bufferedTime >= interval do 769 | bufferedTime = bufferedTime - interval 770 | update(system, interval) 771 | end 772 | system.bufferedTime = bufferedTime 773 | else 774 | update(system, dt) 775 | end 776 | end 777 | 778 | system.modified = false 779 | end 780 | end 781 | 782 | -- Iterate through Systems IN ORDER AGAIN 783 | for i = 1, #systems do 784 | local system = systems[i] 785 | local postWrap = system.postWrap 786 | if postWrap and system.active and 787 | ((not filter) or filter(world, system)) then 788 | postWrap(system, dt) 789 | end 790 | end 791 | 792 | end 793 | 794 | --- Removes all Entities from the World. 795 | function tiny.clearEntities(world) 796 | local el = world.entities 797 | for i = 1, #el do 798 | tiny_removeEntity(world, el[i]) 799 | end 800 | end 801 | 802 | --- Removes all Systems from the World. 803 | function tiny.clearSystems(world) 804 | local systems = world.systems 805 | for i = #systems, 1, -1 do 806 | tiny_removeSystem(world, systems[i]) 807 | end 808 | end 809 | 810 | --- Gets number of Entities in the World. 811 | function tiny.getEntityCount(world) 812 | return #world.entities 813 | end 814 | 815 | --- Gets number of Systems in World. 816 | function tiny.getSystemCount(world) 817 | return #world.systems 818 | end 819 | 820 | --- Sets the index of a System in the World, and returns the old index. Changes 821 | -- the order in which they Systems processed, because lower indexed Systems are 822 | -- processed first. Returns the old system.index. 823 | function tiny.setSystemIndex(world, system, index) 824 | local oldIndex = system.index 825 | local systems = world.systems 826 | 827 | if index < 0 then 828 | index = tiny.getSystemCount(world) + 1 + index 829 | end 830 | 831 | tremove(systems, oldIndex) 832 | tinsert(systems, index, system) 833 | 834 | for i = oldIndex, index, index >= oldIndex and 1 or -1 do 835 | systems[i].index = i 836 | end 837 | 838 | return oldIndex 839 | end 840 | 841 | -- Construct world metatable. 842 | worldMetaTable = { 843 | __index = { 844 | add = tiny.add, 845 | addEntity = tiny.addEntity, 846 | addSystem = tiny.addSystem, 847 | remove = tiny.remove, 848 | removeEntity = tiny.removeEntity, 849 | removeSystem = tiny.removeSystem, 850 | refresh = tiny.refresh, 851 | update = tiny.update, 852 | clearEntities = tiny.clearEntities, 853 | clearSystems = tiny.clearSystems, 854 | getEntityCount = tiny.getEntityCount, 855 | getSystemCount = tiny.getSystemCount, 856 | setSystemIndex = tiny.setSystemIndex 857 | }, 858 | __tostring = function() 859 | return "" 860 | end 861 | } 862 | 863 | return tiny 864 | -------------------------------------------------------------------------------- /lib/sti/init.lua: -------------------------------------------------------------------------------- 1 | --- Simple and fast Tiled map loader and renderer. 2 | -- @module sti 3 | -- @author Landon Manning 4 | -- @copyright 2016 5 | -- @license MIT/X11 6 | 7 | local STI = { 8 | _LICENSE = "MIT/X11", 9 | _URL = "https://github.com/karai17/Simple-Tiled-Implementation", 10 | _VERSION = "0.18.2.1", 11 | _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.", 12 | cache = {} 13 | } 14 | STI.__index = STI 15 | 16 | local cwd = (...):gsub('%.init$', '') .. "." 17 | local utils = require(cwd .. "utils") 18 | local ceil = math.ceil 19 | local floor = math.floor 20 | local lg = require(cwd .. "graphics") 21 | local Map = {} 22 | Map.__index = Map 23 | 24 | local function new(map, plugins, ox, oy) 25 | local dir = "" 26 | 27 | if type(map) == "table" then 28 | map = setmetatable(map, Map) 29 | else 30 | -- Check for valid map type 31 | local ext = map:sub(-4, -1) 32 | assert(ext == ".lua", string.format( 33 | "Invalid file type: %s. File must be of type: lua.", 34 | ext 35 | )) 36 | 37 | -- Get directory of map 38 | dir = map:reverse():find("[/\\]") or "" 39 | if dir ~= "" then 40 | dir = map:sub(1, 1 + (#map - dir)) 41 | end 42 | 43 | -- Load map 44 | map = setmetatable(assert(love.filesystem.load(map))(), Map) 45 | end 46 | 47 | map:init(dir, plugins, ox, oy) 48 | 49 | return map 50 | end 51 | 52 | --- Instance a new map. 53 | -- @param map Path to the map file or the map table itself 54 | -- @param plugins A list of plugins to load 55 | -- @param ox Offset of map on the X axis (in pixels) 56 | -- @param oy Offset of map on the Y axis (in pixels) 57 | -- @return table The loaded Map 58 | function STI.__call(_, map, plugins, ox, oy) 59 | return new(map, plugins, ox, oy) 60 | end 61 | 62 | --- Flush image cache. 63 | function STI:flush() 64 | self.cache = {} 65 | end 66 | 67 | --- Map object 68 | 69 | --- Instance a new map 70 | -- @param path Path to the map file 71 | -- @param plugins A list of plugins to load 72 | -- @param ox Offset of map on the X axis (in pixels) 73 | -- @param oy Offset of map on the Y axis (in pixels) 74 | function Map:init(path, plugins, ox, oy) 75 | if type(plugins) == "table" then 76 | self:loadPlugins(plugins) 77 | end 78 | 79 | self:resize() 80 | self.objects = {} 81 | self.tiles = {} 82 | self.tileInstances = {} 83 | self.drawRange = { 84 | sx = 1, 85 | sy = 1, 86 | ex = self.width, 87 | ey = self.height, 88 | } 89 | self.offsetx = ox or 0 90 | self.offsety = oy or 0 91 | 92 | self.freeBatchSprites = {} 93 | setmetatable(self.freeBatchSprites, { __mode = 'k' }) 94 | 95 | -- Set tiles, images 96 | local gid = 1 97 | for i, tileset in ipairs(self.tilesets) do 98 | assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.") 99 | 100 | -- Cache images 101 | if lg.isCreated then 102 | local formatted_path = utils.format_path(path .. tileset.image) 103 | 104 | if not STI.cache[formatted_path] then 105 | utils.fix_transparent_color(tileset, formatted_path) 106 | utils.cache_image(STI, formatted_path, tileset.image) 107 | else 108 | tileset.image = STI.cache[formatted_path] 109 | end 110 | end 111 | 112 | gid = self:setTiles(i, tileset, gid) 113 | end 114 | 115 | -- Set layers 116 | for _, layer in ipairs(self.layers) do 117 | self:setLayer(layer, path) 118 | end 119 | end 120 | 121 | --- Load plugins 122 | -- @param plugins A list of plugins to load 123 | function Map:loadPlugins(plugins) 124 | for _, plugin in ipairs(plugins) do 125 | local pluginModulePath = cwd .. 'plugins.' .. plugin 126 | local ok, pluginModule = pcall(require, pluginModulePath) 127 | if ok then 128 | for k, func in pairs(pluginModule) do 129 | if not self[k] then 130 | self[k] = func 131 | end 132 | end 133 | end 134 | end 135 | end 136 | 137 | --- Create Tiles 138 | -- @param index Index of the Tileset 139 | -- @param tileset Tileset data 140 | -- @param gid First Global ID in Tileset 141 | -- @return number Next Tileset's first Global ID 142 | function Map:setTiles(index, tileset, gid) 143 | local quad = lg.newQuad 144 | local imageW = tileset.imagewidth 145 | local imageH = tileset.imageheight 146 | local tileW = tileset.tilewidth 147 | local tileH = tileset.tileheight 148 | local margin = tileset.margin 149 | local spacing = tileset.spacing 150 | local w = utils.get_tiles(imageW, tileW, margin, spacing) 151 | local h = utils.get_tiles(imageH, tileH, margin, spacing) 152 | 153 | for y = 1, h do 154 | for x = 1, w do 155 | local id = gid - tileset.firstgid 156 | local quadX = (x - 1) * tileW + margin + (x - 1) * spacing 157 | local quadY = (y - 1) * tileH + margin + (y - 1) * spacing 158 | local properties, terrain, animation, objectGroup 159 | 160 | for _, tile in pairs(tileset.tiles) do 161 | if tile.id == id then 162 | properties = tile.properties 163 | animation = tile.animation 164 | objectGroup = tile.objectGroup 165 | 166 | if tile.terrain then 167 | terrain = {} 168 | 169 | for i = 1, #tile.terrain do 170 | terrain[i] = tileset.terrains[tile.terrain[i] + 1] 171 | end 172 | end 173 | end 174 | end 175 | 176 | local tile = { 177 | id = id, 178 | gid = gid, 179 | tileset = index, 180 | quad = quad( 181 | quadX, quadY, 182 | tileW, tileH, 183 | imageW, imageH 184 | ), 185 | properties = properties or {}, 186 | terrain = terrain, 187 | animation = animation, 188 | objectGroup = objectGroup, 189 | frame = 1, 190 | time = 0, 191 | width = tileW, 192 | height = tileH, 193 | sx = 1, 194 | sy = 1, 195 | r = 0, 196 | offset = tileset.tileoffset, 197 | } 198 | 199 | self.tiles[gid] = tile 200 | gid = gid + 1 201 | end 202 | end 203 | 204 | return gid 205 | end 206 | 207 | --- Create Layers 208 | -- @param layer Layer data 209 | -- @param path (Optional) Path to an Image Layer's image 210 | function Map:setLayer(layer, path) 211 | if layer.encoding then 212 | if layer.encoding == "base64" then 213 | assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".") 214 | local fd = love.data.decode("string", "base64", layer.data) 215 | 216 | if not layer.compression then 217 | layer.data = utils.get_decompressed_data(fd) 218 | else 219 | assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".") 220 | 221 | if layer.compression == "zlib" then 222 | local data = love.data.decompress("string", "zlib", fd) 223 | layer.data = utils.get_decompressed_data(data) 224 | end 225 | 226 | if layer.compression == "gzip" then 227 | local data = love.data.decompress("string", "gzip", fd) 228 | layer.data = utils.get_decompressed_data(data) 229 | end 230 | end 231 | end 232 | end 233 | 234 | layer.x = (layer.x or 0) + layer.offsetx + self.offsetx 235 | layer.y = (layer.y or 0) + layer.offsety + self.offsety 236 | layer.update = function() end 237 | 238 | if layer.type == "tilelayer" then 239 | self:setTileData(layer) 240 | self:setSpriteBatches(layer) 241 | layer.draw = function() self:drawTileLayer(layer) end 242 | elseif layer.type == "objectgroup" then 243 | self:setObjectData(layer) 244 | self:setObjectCoordinates(layer) 245 | self:setObjectSpriteBatches(layer) 246 | layer.draw = function() self:drawObjectLayer(layer) end 247 | elseif layer.type == "imagelayer" then 248 | layer.draw = function() self:drawImageLayer(layer) end 249 | 250 | if layer.image ~= "" then 251 | local formatted_path = utils.format_path(path .. layer.image) 252 | if not STI.cache[formatted_path] then 253 | utils.cache_image(STI, formatted_path) 254 | end 255 | 256 | layer.image = STI.cache[formatted_path] 257 | layer.width = layer.image:getWidth() 258 | layer.height = layer.image:getHeight() 259 | end 260 | end 261 | 262 | self.layers[layer.name] = layer 263 | end 264 | 265 | --- Add Tiles to Tile Layer 266 | -- @param layer The Tile Layer 267 | function Map:setTileData(layer) 268 | local i = 1 269 | local map = {} 270 | 271 | for y = 1, layer.height do 272 | map[y] = {} 273 | for x = 1, layer.width do 274 | local gid = layer.data[i] 275 | 276 | if gid > 0 then 277 | map[y][x] = self.tiles[gid] or self:setFlippedGID(gid) 278 | end 279 | 280 | i = i + 1 281 | end 282 | end 283 | 284 | layer.data = map 285 | end 286 | 287 | --- Add Objects to Layer 288 | -- @param layer The Object Layer 289 | function Map:setObjectData(layer) 290 | for _, object in ipairs(layer.objects) do 291 | object.layer = layer 292 | self.objects[object.id] = object 293 | end 294 | end 295 | 296 | --- Correct position and orientation of Objects in an Object Layer 297 | -- @param layer The Object Layer 298 | function Map:setObjectCoordinates(layer) 299 | for _, object in ipairs(layer.objects) do 300 | local x = layer.x + object.x 301 | local y = layer.y + object.y 302 | local w = object.width 303 | local h = object.height 304 | local r = object.rotation 305 | local cos = math.cos(math.rad(r)) 306 | local sin = math.sin(math.rad(r)) 307 | 308 | if object.shape == "rectangle" and not object.gid then 309 | object.rectangle = {} 310 | 311 | local vertices = { 312 | { x=x, y=y }, 313 | { x=x + w, y=y }, 314 | { x=x + w, y=y + h }, 315 | { x=x, y=y + h }, 316 | } 317 | 318 | for _, vertex in ipairs(vertices) do 319 | vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) 320 | table.insert(object.rectangle, { x = vertex.x, y = vertex.y }) 321 | end 322 | elseif object.shape == "ellipse" then 323 | object.ellipse = {} 324 | local vertices = utils.convert_ellipse_to_polygon(x, y, w, h) 325 | 326 | for _, vertex in ipairs(vertices) do 327 | vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) 328 | table.insert(object.ellipse, { x = vertex.x, y = vertex.y }) 329 | end 330 | elseif object.shape == "polygon" then 331 | for _, vertex in ipairs(object.polygon) do 332 | vertex.x = vertex.x + x 333 | vertex.y = vertex.y + y 334 | vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) 335 | end 336 | elseif object.shape == "polyline" then 337 | for _, vertex in ipairs(object.polyline) do 338 | vertex.x = vertex.x + x 339 | vertex.y = vertex.y + y 340 | vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) 341 | end 342 | end 343 | end 344 | end 345 | 346 | --- Convert tile location to tile instance location 347 | -- @param layer Tile layer 348 | -- @param tile Tile 349 | -- @param x Tile location on X axis (in tiles) 350 | -- @param y Tile location on Y axis (in tiles) 351 | -- @return number Tile instance location on X axis (in pixels) 352 | -- @return number Tile instance location on Y axis (in pixels) 353 | function Map:getLayerTilePosition(layer, tile, x, y) 354 | local tileW = self.tilewidth 355 | local tileH = self.tileheight 356 | local tileX, tileY 357 | 358 | if self.orientation == "orthogonal" then 359 | local tileset = self.tilesets[tile.tileset] 360 | tileX = (x - 1) * tileW + tile.offset.x 361 | tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight 362 | tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH) 363 | elseif self.orientation == "isometric" then 364 | tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2 365 | tileY = (x + y - 2) * (tileH / 2) + tile.offset.y 366 | else 367 | local sideLen = self.hexsidelength or 0 368 | if self.staggeraxis == "y" then 369 | if self.staggerindex == "odd" then 370 | if y % 2 == 0 then 371 | tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x 372 | else 373 | tileX = (x - 1) * tileW + tile.offset.x 374 | end 375 | else 376 | if y % 2 == 0 then 377 | tileX = (x - 1) * tileW + tile.offset.x 378 | else 379 | tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x 380 | end 381 | end 382 | 383 | local rowH = tileH - (tileH - sideLen) / 2 384 | tileY = (y - 1) * rowH + tile.offset.y 385 | else 386 | if self.staggerindex == "odd" then 387 | if x % 2 == 0 then 388 | tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y 389 | else 390 | tileY = (y - 1) * tileH + tile.offset.y 391 | end 392 | else 393 | if x % 2 == 0 then 394 | tileY = (y - 1) * tileH + tile.offset.y 395 | else 396 | tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y 397 | end 398 | end 399 | 400 | local colW = tileW - (tileW - sideLen) / 2 401 | tileX = (x - 1) * colW + tile.offset.x 402 | end 403 | end 404 | 405 | return tileX, tileY 406 | end 407 | 408 | --- Place new tile instance 409 | -- @param layer Tile layer 410 | -- @param tile Tile 411 | -- @param number Tile location on X axis (in tiles) 412 | -- @param number Tile location on Y axis (in tiles) 413 | function Map:addNewLayerTile(layer, tile, x, y) 414 | local tileset = tile.tileset 415 | local image = self.tilesets[tile.tileset].image 416 | 417 | layer.batches[tileset] = layer.batches[tileset] 418 | or lg.newSpriteBatch(image, layer.width * layer.height) 419 | 420 | local batch = layer.batches[tileset] 421 | local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) 422 | 423 | local tab = { 424 | layer = layer, 425 | gid = tile.gid, 426 | x = tileX, 427 | y = tileY, 428 | r = tile.r, 429 | oy = 0 430 | } 431 | 432 | if batch then 433 | tab.batch = batch 434 | tab.id = batch:add(tile.quad, tileX, tileY, tile.r, tile.sx, tile.sy) 435 | end 436 | 437 | self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} 438 | table.insert(self.tileInstances[tile.gid], tab) 439 | end 440 | 441 | --- Batch Tiles in Tile Layer for improved draw speed 442 | -- @param layer The Tile Layer 443 | function Map:setSpriteBatches(layer) 444 | layer.batches = {} 445 | 446 | if self.orientation == "orthogonal" or self.orientation == "isometric" then 447 | local startX = 1 448 | local startY = 1 449 | local endX = layer.width 450 | local endY = layer.height 451 | local incrementX = 1 452 | local incrementY = 1 453 | 454 | -- Determine order to add tiles to sprite batch 455 | -- Defaults to right-down 456 | if self.renderorder == "right-up" then 457 | startX, endX, incrementX = startX, endX, 1 458 | startY, endY, incrementY = endY, startY, -1 459 | elseif self.renderorder == "left-down" then 460 | startX, endX, incrementX = endX, startX, -1 461 | startY, endY, incrementY = startY, endY, 1 462 | elseif self.renderorder == "left-up" then 463 | startX, endX, incrementX = endX, startX, -1 464 | startY, endY, incrementY = endY, startY, -1 465 | end 466 | 467 | for y = startY, endY, incrementY do 468 | for x = startX, endX, incrementX do 469 | local tile = layer.data[y][x] 470 | 471 | if tile then 472 | self:addNewLayerTile(layer, tile, x, y) 473 | end 474 | end 475 | end 476 | else 477 | local sideLen = self.hexsidelength or 0 478 | 479 | if self.staggeraxis == "y" then 480 | for y = 1, layer.height do 481 | for x = 1, layer.width do 482 | local tile = layer.data[y][x] 483 | 484 | if tile then 485 | self:addNewLayerTile(layer, tile, x, y) 486 | end 487 | end 488 | end 489 | else 490 | local i = 0 491 | local _x 492 | 493 | if self.staggerindex == "odd" then 494 | _x = 1 495 | else 496 | _x = 2 497 | end 498 | 499 | while i < layer.width * layer.height do 500 | for _y = 1, layer.height + 0.5, 0.5 do 501 | local y = floor(_y) 502 | 503 | for x = _x, layer.width, 2 do 504 | i = i + 1 505 | local tile = layer.data[y][x] 506 | 507 | if tile then 508 | self:addNewLayerTile(layer, tile, x, y) 509 | end 510 | end 511 | 512 | if _x == 1 then 513 | _x = 2 514 | else 515 | _x = 1 516 | end 517 | end 518 | end 519 | end 520 | end 521 | end 522 | 523 | --- Batch Tiles in Object Layer for improved draw speed 524 | -- @param layer The Object Layer 525 | function Map:setObjectSpriteBatches(layer) 526 | local newBatch = lg.newSpriteBatch 527 | local tileW = self.tilewidth 528 | local tileH = self.tileheight 529 | local batches = {} 530 | 531 | if layer.draworder == "topdown" then 532 | table.sort(layer.objects, function(a, b) 533 | return a.y + a.height < b.y + b.height 534 | end) 535 | end 536 | 537 | for _, object in ipairs(layer.objects) do 538 | if object.gid then 539 | local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) 540 | local tileset = tile.tileset 541 | local image = self.tilesets[tile.tileset].image 542 | 543 | batches[tileset] = batches[tileset] or newBatch(image) 544 | 545 | local sx = object.width / tile.width 546 | local sy = object.height / tile.height 547 | 548 | local batch = batches[tileset] 549 | local tileX = object.x + tile.offset.x 550 | local tileY = object.y + tile.offset.y - tile.height * sy 551 | local tileR = math.rad(object.rotation) 552 | local oy = 0 553 | 554 | -- Compensation for scale/rotation shift 555 | if tile.sx == 1 and tile.sy == 1 then 556 | if tileR ~= 0 then 557 | tileY = tileY + tileH 558 | oy = tileH 559 | end 560 | else 561 | if tile.sx < 0 then tileX = tileX + tileW end 562 | if tile.sy < 0 then tileY = tileY + tileH end 563 | if tileR > 0 then tileX = tileX + tileW end 564 | if tileR < 0 then tileY = tileY + tileH end 565 | end 566 | 567 | local tab = { 568 | layer = layer, 569 | gid = tile.gid, 570 | x = tileX, 571 | y = tileY, 572 | r = tileR, 573 | oy = oy 574 | } 575 | 576 | if batch then 577 | tab.batch = batch 578 | tab.id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, 0, oy) 579 | end 580 | 581 | self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} 582 | table.insert(self.tileInstances[tile.gid], tab) 583 | end 584 | end 585 | 586 | layer.batches = batches 587 | end 588 | 589 | --- Create a Custom Layer to place userdata in (such as player sprites) 590 | -- @param name Name of Custom Layer 591 | -- @param index Draw order within Layer stack 592 | -- @return table Custom Layer 593 | function Map:addCustomLayer(name, index) 594 | index = index or #self.layers + 1 595 | local layer = { 596 | type = "customlayer", 597 | name = name, 598 | visible = true, 599 | opacity = 1, 600 | properties = {}, 601 | } 602 | 603 | function layer.draw() end 604 | function layer.update() end 605 | 606 | table.insert(self.layers, index, layer) 607 | self.layers[name] = self.layers[index] 608 | 609 | return layer 610 | end 611 | 612 | --- Convert another Layer into a Custom Layer 613 | -- @param index Index or name of Layer to convert 614 | -- @return table Custom Layer 615 | function Map:convertToCustomLayer(index) 616 | local layer = assert(self.layers[index], "Layer not found: " .. index) 617 | 618 | layer.type = "customlayer" 619 | layer.x = nil 620 | layer.y = nil 621 | layer.width = nil 622 | layer.height = nil 623 | layer.encoding = nil 624 | layer.data = nil 625 | layer.objects = nil 626 | layer.image = nil 627 | 628 | function layer.draw() end 629 | function layer.update() end 630 | 631 | return layer 632 | end 633 | 634 | --- Remove a Layer from the Layer stack 635 | -- @param index Index or name of Layer to convert 636 | function Map:removeLayer(index) 637 | local layer = assert(self.layers[index], "Layer not found: " .. index) 638 | 639 | if type(index) == "string" then 640 | for i, l in ipairs(self.layers) do 641 | if l.name == index then 642 | table.remove(self.layers, i) 643 | self.layers[index] = nil 644 | break 645 | end 646 | end 647 | else 648 | local name = self.layers[index].name 649 | table.remove(self.layers, index) 650 | self.layers[name] = nil 651 | end 652 | 653 | -- Remove tile instances 654 | if layer.batches then 655 | for _, batch in pairs(layer.batches) do 656 | self.freeBatchSprites[batch] = nil 657 | end 658 | 659 | for _, tiles in pairs(self.tileInstances) do 660 | for i = #tiles, 1, -1 do 661 | local tile = tiles[i] 662 | if tile.layer == layer then 663 | table.remove(tiles, i) 664 | end 665 | end 666 | end 667 | end 668 | 669 | -- Remove objects 670 | if layer.objects then 671 | for i, object in pairs(self.objects) do 672 | if object.layer == layer then 673 | self.objects[i] = nil 674 | end 675 | end 676 | end 677 | end 678 | 679 | --- Animate Tiles and update every Layer 680 | -- @param dt Delta Time 681 | function Map:update(dt) 682 | for _, tile in pairs(self.tiles) do 683 | local update = false 684 | 685 | if tile.animation then 686 | tile.time = tile.time + dt * 1000 687 | 688 | while tile.time > tonumber(tile.animation[tile.frame].duration) do 689 | update = true 690 | tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) 691 | tile.frame = tile.frame + 1 692 | 693 | if tile.frame > #tile.animation then tile.frame = 1 end 694 | end 695 | 696 | if update and self.tileInstances[tile.gid] then 697 | for _, j in pairs(self.tileInstances[tile.gid]) do 698 | local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] 699 | j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy) 700 | end 701 | end 702 | end 703 | end 704 | 705 | for _, layer in ipairs(self.layers) do 706 | layer:update(dt) 707 | end 708 | end 709 | 710 | --- Draw every Layer 711 | -- @param tx Translate on X 712 | -- @param ty Translate on Y 713 | -- @param sx Scale on X 714 | -- @param sy Scale on Y 715 | function Map:draw(tx, ty, sx, sy) 716 | local current_canvas = lg.getCanvas() 717 | lg.setCanvas(self.canvas) 718 | lg.clear() 719 | 720 | -- Scale map to 1.0 to draw onto canvas, this fixes tearing issues 721 | -- Map is translated to correct position so the right section is drawn 722 | lg.push() 723 | lg.origin() 724 | lg.translate(math.floor(tx or 0), math.floor(ty or 0)) 725 | 726 | for _, layer in ipairs(self.layers) do 727 | if layer.visible and layer.opacity > 0 then 728 | self:drawLayer(layer) 729 | end 730 | end 731 | 732 | lg.pop() 733 | 734 | -- Draw canvas at 0,0; this fixes scissoring issues 735 | -- Map is scaled to correct scale so the right section is shown 736 | lg.push() 737 | lg.origin() 738 | lg.scale(sx or 1, sy or sx or 1) 739 | 740 | lg.setCanvas(current_canvas) 741 | lg.draw(self.canvas) 742 | 743 | lg.pop() 744 | end 745 | 746 | --- Draw an individual Layer 747 | -- @param layer The Layer to draw 748 | function Map.drawLayer(_, layer) 749 | local r,g,b,a = lg.getColor() 750 | lg.setColor(r, g, b, a * layer.opacity) 751 | layer:draw() 752 | lg.setColor(r,g,b,a) 753 | end 754 | 755 | --- Default draw function for Tile Layers 756 | -- @param layer The Tile Layer to draw 757 | function Map:drawTileLayer(layer) 758 | if type(layer) == "string" or type(layer) == "number" then 759 | layer = self.layers[layer] 760 | end 761 | 762 | assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer") 763 | 764 | for _, batch in pairs(layer.batches) do 765 | lg.draw(batch, floor(layer.x), floor(layer.y)) 766 | end 767 | end 768 | 769 | --- Default draw function for Object Layers 770 | -- @param layer The Object Layer to draw 771 | function Map:drawObjectLayer(layer) 772 | if type(layer) == "string" or type(layer) == "number" then 773 | layer = self.layers[layer] 774 | end 775 | 776 | assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup") 777 | 778 | local line = { 160, 160, 160, 255 * layer.opacity } 779 | local fill = { 160, 160, 160, 255 * layer.opacity * 0.5 } 780 | local r,g,b,a = lg.getColor() 781 | local reset = { r, g, b, a * layer.opacity } 782 | 783 | local function sortVertices(obj) 784 | local vertex = {} 785 | 786 | for _, v in ipairs(obj) do 787 | table.insert(vertex, v.x) 788 | table.insert(vertex, v.y) 789 | end 790 | 791 | return vertex 792 | end 793 | 794 | local function drawShape(obj, shape) 795 | local vertex = sortVertices(obj) 796 | 797 | if shape == "polyline" then 798 | lg.setColor(line) 799 | lg.line(vertex) 800 | return 801 | elseif shape == "polygon" then 802 | lg.setColor(fill) 803 | if not love.math.isConvex(vertex) then 804 | local triangles = love.math.triangulate(vertex) 805 | for _, triangle in ipairs(triangles) do 806 | lg.polygon("fill", triangle) 807 | end 808 | else 809 | lg.polygon("fill", vertex) 810 | end 811 | else 812 | lg.setColor(fill) 813 | lg.polygon("fill", vertex) 814 | end 815 | 816 | lg.setColor(line) 817 | lg.polygon("line", vertex) 818 | end 819 | 820 | for _, object in ipairs(layer.objects) do 821 | if object.shape == "rectangle" and not object.gid then 822 | drawShape(object.rectangle, "rectangle") 823 | elseif object.shape == "ellipse" then 824 | drawShape(object.ellipse, "ellipse") 825 | elseif object.shape == "polygon" then 826 | drawShape(object.polygon, "polygon") 827 | elseif object.shape == "polyline" then 828 | drawShape(object.polyline, "polyline") 829 | end 830 | end 831 | 832 | lg.setColor(reset) 833 | for _, batch in pairs(layer.batches) do 834 | lg.draw(batch, 0, 0) 835 | end 836 | lg.setColor(r,g,b,a) 837 | end 838 | 839 | --- Default draw function for Image Layers 840 | -- @param layer The Image Layer to draw 841 | function Map:drawImageLayer(layer) 842 | if type(layer) == "string" or type(layer) == "number" then 843 | layer = self.layers[layer] 844 | end 845 | 846 | assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer") 847 | 848 | if layer.image ~= "" then 849 | lg.draw(layer.image, layer.x, layer.y) 850 | end 851 | end 852 | 853 | --- Resize the drawable area of the Map 854 | -- @param w The new width of the drawable area (in pixels) 855 | -- @param h The new Height of the drawable area (in pixels) 856 | function Map:resize(w, h) 857 | if lg.isCreated then 858 | w = w or lg.getWidth() 859 | h = h or lg.getHeight() 860 | 861 | self.canvas = lg.newCanvas(w, h) 862 | self.canvas:setFilter("nearest", "nearest") 863 | end 864 | end 865 | 866 | --- Create flipped or rotated Tiles based on bitop flags 867 | -- @param gid The flagged Global ID 868 | -- @return table Flipped Tile 869 | function Map:setFlippedGID(gid) 870 | local bit31 = 2147483648 871 | local bit30 = 1073741824 872 | local bit29 = 536870912 873 | local flipX = false 874 | local flipY = false 875 | local flipD = false 876 | local realgid = gid 877 | 878 | if realgid >= bit31 then 879 | realgid = realgid - bit31 880 | flipX = not flipX 881 | end 882 | 883 | if realgid >= bit30 then 884 | realgid = realgid - bit30 885 | flipY = not flipY 886 | end 887 | 888 | if realgid >= bit29 then 889 | realgid = realgid - bit29 890 | flipD = not flipD 891 | end 892 | 893 | local tile = self.tiles[realgid] 894 | local data = { 895 | id = tile.id, 896 | gid = gid, 897 | tileset = tile.tileset, 898 | frame = tile.frame, 899 | time = tile.time, 900 | width = tile.width, 901 | height = tile.height, 902 | offset = tile.offset, 903 | quad = tile.quad, 904 | properties = tile.properties, 905 | terrain = tile.terrain, 906 | animation = tile.animation, 907 | sx = tile.sx, 908 | sy = tile.sy, 909 | r = tile.r, 910 | } 911 | 912 | if flipX then 913 | if flipY and flipD then 914 | data.r = math.rad(-90) 915 | data.sy = -1 916 | elseif flipY then 917 | data.sx = -1 918 | data.sy = -1 919 | elseif flipD then 920 | data.r = math.rad(90) 921 | else 922 | data.sx = -1 923 | end 924 | elseif flipY then 925 | if flipD then 926 | data.r = math.rad(-90) 927 | else 928 | data.sy = -1 929 | end 930 | elseif flipD then 931 | data.r = math.rad(90) 932 | data.sy = -1 933 | end 934 | 935 | self.tiles[gid] = data 936 | 937 | return self.tiles[gid] 938 | end 939 | 940 | --- Get custom properties from Layer 941 | -- @param layer The Layer 942 | -- @return table List of properties 943 | function Map:getLayerProperties(layer) 944 | local l = self.layers[layer] 945 | 946 | if not l then 947 | return {} 948 | end 949 | 950 | return l.properties 951 | end 952 | 953 | --- Get custom properties from Tile 954 | -- @param layer The Layer that the Tile belongs to 955 | -- @param x The X axis location of the Tile (in tiles) 956 | -- @param y The Y axis location of the Tile (in tiles) 957 | -- @return table List of properties 958 | function Map:getTileProperties(layer, x, y) 959 | local tile = self.layers[layer].data[y][x] 960 | 961 | if not tile then 962 | return {} 963 | end 964 | 965 | return tile.properties 966 | end 967 | 968 | --- Get custom properties from Object 969 | -- @param layer The Layer that the Object belongs to 970 | -- @param object The index or name of the Object 971 | -- @return table List of properties 972 | function Map:getObjectProperties(layer, object) 973 | local o = self.layers[layer].objects 974 | 975 | if type(object) == "number" then 976 | o = o[object] 977 | else 978 | for _, v in ipairs(o) do 979 | if v.name == object then 980 | o = v 981 | break 982 | end 983 | end 984 | end 985 | 986 | if not o then 987 | return {} 988 | end 989 | 990 | return o.properties 991 | end 992 | 993 | --- Change a tile in a layer to another tile 994 | -- @param layer The Layer that the Tile belongs to 995 | -- @param x The X axis location of the Tile (in tiles) 996 | -- @param y The Y axis location of the Tile (in tiles) 997 | -- @param gid The gid of the new tile 998 | function Map:setLayerTile(layer, x, y, gid) 999 | layer = self.layers[layer] 1000 | 1001 | layer.data[y] = layer.data[y] or {} 1002 | local tile = layer.data[y][x] 1003 | local instance 1004 | if tile then 1005 | local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) 1006 | for _, inst in pairs(self.tileInstances[tile.gid]) do 1007 | if inst.x == tileX and inst.y == tileY then 1008 | instance = inst 1009 | break 1010 | end 1011 | end 1012 | end 1013 | 1014 | if tile == self.tiles[gid] then 1015 | return 1016 | end 1017 | 1018 | tile = self.tiles[gid] 1019 | 1020 | if instance then 1021 | self:swapTile(instance, tile) 1022 | else 1023 | self:addNewLayerTile(layer, tile, x, y) 1024 | end 1025 | layer.data[y][x] = tile 1026 | end 1027 | 1028 | --- Swap a tile in a spritebatch 1029 | -- @param instance The current Instance object we want to replace 1030 | -- @param tile The Tile object we want to use 1031 | -- @return none 1032 | function Map:swapTile(instance, tile) 1033 | -- Update sprite batch 1034 | if instance.batch then 1035 | if tile then 1036 | instance.batch:set( 1037 | instance.id, 1038 | tile.quad, 1039 | instance.x, 1040 | instance.y, 1041 | tile.r, 1042 | tile.sx, 1043 | tile.sy 1044 | ) 1045 | else 1046 | instance.batch:set( 1047 | instance.id, 1048 | instance.x, 1049 | instance.y, 1050 | 0, 1051 | 0) 1052 | 1053 | self.freeBatchSprites[instance.batch] = 1054 | self.freeBatchSprites[instance.batch] or {} 1055 | 1056 | table.insert(self.freeBatchSprites[instance.batch], instance) 1057 | end 1058 | end 1059 | 1060 | -- Remove old tile instance 1061 | for i, ins in ipairs(self.tileInstances[instance.gid]) do 1062 | if ins.batch == instance.batch and ins.id == instance.id then 1063 | table.remove(self.tileInstances[instance.gid], i) 1064 | break 1065 | end 1066 | end 1067 | 1068 | -- Add new tile instance 1069 | if tile then 1070 | self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} 1071 | 1072 | local freeBatchSprites = self.freeBatchSprites[instance.batch] 1073 | local newInstance 1074 | if freeBatchSprites and #freeBatchSprites > 0 then 1075 | newInstance = freeBatchSprites[#freeBatchSprites] 1076 | freeBatchSprites[#freeBatchSprites] = nil 1077 | else 1078 | newInstance = {} 1079 | end 1080 | 1081 | newInstance.layer = instance.layer 1082 | newInstance.batch = instance.batch 1083 | newInstance.id = instance.id 1084 | newInstance.gid = tile.gid or 0 1085 | newInstance.x = instance.x 1086 | newInstance.y = instance.y 1087 | newInstance.r = tile.r or 0 1088 | newInstance.oy = tile.r ~= 0 and tile.height or 0 1089 | table.insert(self.tileInstances[tile.gid], newInstance) 1090 | end 1091 | end 1092 | 1093 | --- Convert tile location to pixel location 1094 | -- @param x The X axis location of the point (in tiles) 1095 | -- @param y The Y axis location of the point (in tiles) 1096 | -- @return number The X axis location of the point (in pixels) 1097 | -- @return number The Y axis location of the point (in pixels) 1098 | function Map:convertTileToPixel(x,y) 1099 | if self.orientation == "orthogonal" then 1100 | local tileW = self.tilewidth 1101 | local tileH = self.tileheight 1102 | return 1103 | x * tileW, 1104 | y * tileH 1105 | elseif self.orientation == "isometric" then 1106 | local mapH = self.height 1107 | local tileW = self.tilewidth 1108 | local tileH = self.tileheight 1109 | local offsetX = mapH * tileW / 2 1110 | return 1111 | (x - y) * tileW / 2 + offsetX, 1112 | (x + y) * tileH / 2 1113 | elseif self.orientation == "staggered" or 1114 | self.orientation == "hexagonal" then 1115 | local tileW = self.tilewidth 1116 | local tileH = self.tileheight 1117 | local sideLen = self.hexsidelength or 0 1118 | 1119 | if self.staggeraxis == "x" then 1120 | return 1121 | x * tileW, 1122 | ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0) 1123 | else 1124 | return 1125 | ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0), 1126 | y * tileH 1127 | end 1128 | end 1129 | end 1130 | 1131 | --- Convert pixel location to tile location 1132 | -- @param x The X axis location of the point (in pixels) 1133 | -- @param y The Y axis location of the point (in pixels) 1134 | -- @return number The X axis location of the point (in tiles) 1135 | -- @return number The Y axis location of the point (in tiles) 1136 | function Map:convertPixelToTile(x, y) 1137 | if self.orientation == "orthogonal" then 1138 | local tileW = self.tilewidth 1139 | local tileH = self.tileheight 1140 | return 1141 | x / tileW, 1142 | y / tileH 1143 | elseif self.orientation == "isometric" then 1144 | local mapH = self.height 1145 | local tileW = self.tilewidth 1146 | local tileH = self.tileheight 1147 | local offsetX = mapH * tileW / 2 1148 | return 1149 | y / tileH + (x - offsetX) / tileW, 1150 | y / tileH - (x - offsetX) / tileW 1151 | elseif self.orientation == "staggered" then 1152 | local staggerX = self.staggeraxis == "x" 1153 | local even = self.staggerindex == "even" 1154 | 1155 | local function topLeft(x, y) 1156 | if staggerX then 1157 | if ceil(x) % 2 == 1 and even then 1158 | return x - 1, y 1159 | else 1160 | return x - 1, y - 1 1161 | end 1162 | else 1163 | if ceil(y) % 2 == 1 and even then 1164 | return x, y - 1 1165 | else 1166 | return x - 1, y - 1 1167 | end 1168 | end 1169 | end 1170 | 1171 | local function topRight(x, y) 1172 | if staggerX then 1173 | if ceil(x) % 2 == 1 and even then 1174 | return x + 1, y 1175 | else 1176 | return x + 1, y - 1 1177 | end 1178 | else 1179 | if ceil(y) % 2 == 1 and even then 1180 | return x + 1, y - 1 1181 | else 1182 | return x, y - 1 1183 | end 1184 | end 1185 | end 1186 | 1187 | local function bottomLeft(x, y) 1188 | if staggerX then 1189 | if ceil(x) % 2 == 1 and even then 1190 | return x - 1, y + 1 1191 | else 1192 | return x - 1, y 1193 | end 1194 | else 1195 | if ceil(y) % 2 == 1 and even then 1196 | return x, y + 1 1197 | else 1198 | return x - 1, y + 1 1199 | end 1200 | end 1201 | end 1202 | 1203 | local function bottomRight(x, y) 1204 | if staggerX then 1205 | if ceil(x) % 2 == 1 and even then 1206 | return x + 1, y + 1 1207 | else 1208 | return x + 1, y 1209 | end 1210 | else 1211 | if ceil(y) % 2 == 1 and even then 1212 | return x + 1, y + 1 1213 | else 1214 | return x, y + 1 1215 | end 1216 | end 1217 | end 1218 | 1219 | local tileW = self.tilewidth 1220 | local tileH = self.tileheight 1221 | 1222 | if staggerX then 1223 | x = x - (even and tileW / 2 or 0) 1224 | else 1225 | y = y - (even and tileH / 2 or 0) 1226 | end 1227 | 1228 | local halfH = tileH / 2 1229 | local ratio = tileH / tileW 1230 | local referenceX = ceil(x / tileW) 1231 | local referenceY = ceil(y / tileH) 1232 | local relativeX = x - referenceX * tileW 1233 | local relativeY = y - referenceY * tileH 1234 | 1235 | if (halfH - relativeX * ratio > relativeY) then 1236 | return topLeft(referenceX, referenceY) 1237 | elseif (-halfH + relativeX * ratio > relativeY) then 1238 | return topRight(referenceX, referenceY) 1239 | elseif (halfH + relativeX * ratio < relativeY) then 1240 | return bottomLeft(referenceX, referenceY) 1241 | elseif (halfH * 3 - relativeX * ratio < relativeY) then 1242 | return bottomRight(referenceX, referenceY) 1243 | end 1244 | 1245 | return referenceX, referenceY 1246 | elseif self.orientation == "hexagonal" then 1247 | local staggerX = self.staggeraxis == "x" 1248 | local even = self.staggerindex == "even" 1249 | local tileW = self.tilewidth 1250 | local tileH = self.tileheight 1251 | local sideLenX = 0 1252 | local sideLenY = 0 1253 | 1254 | if staggerX then 1255 | sideLenX = self.hexsidelength 1256 | x = x - (even and tileW or (tileW - sideLenX) / 2) 1257 | else 1258 | sideLenY = self.hexsidelength 1259 | y = y - (even and tileH or (tileH - sideLenY) / 2) 1260 | end 1261 | 1262 | local colW = ((tileW - sideLenX) / 2) + sideLenX 1263 | local rowH = ((tileH - sideLenY) / 2) + sideLenY 1264 | local referenceX = ceil(x) / (colW * 2) 1265 | local referenceY = ceil(y) / (rowH * 2) 1266 | local relativeX = x - referenceX * colW * 2 1267 | local relativeY = y - referenceY * rowH * 2 1268 | local centers 1269 | 1270 | if staggerX then 1271 | local left = sideLenX / 2 1272 | local centerX = left + colW 1273 | local centerY = tileH / 2 1274 | 1275 | centers = { 1276 | { x = left, y = centerY }, 1277 | { x = centerX, y = centerY - rowH }, 1278 | { x = centerX, y = centerY + rowH }, 1279 | { x = centerX + colW, y = centerY }, 1280 | } 1281 | else 1282 | local top = sideLenY / 2 1283 | local centerX = tileW / 2 1284 | local centerY = top + rowH 1285 | 1286 | centers = { 1287 | { x = centerX, y = top }, 1288 | { x = centerX - colW, y = centerY }, 1289 | { x = centerX + colW, y = centerY }, 1290 | { x = centerX, y = centerY + rowH } 1291 | } 1292 | end 1293 | 1294 | local nearest = 0 1295 | local minDist = math.huge 1296 | 1297 | local function len2(ax, ay) 1298 | return ax * ax + ay * ay 1299 | end 1300 | 1301 | for i = 1, 4 do 1302 | local dc = len2(centers[i].x - relativeX, centers[i].y - relativeY) 1303 | 1304 | if dc < minDist then 1305 | minDist = dc 1306 | nearest = i 1307 | end 1308 | end 1309 | 1310 | local offsetsStaggerX = { 1311 | { x = 0, y = 0 }, 1312 | { x = 1, y = -1 }, 1313 | { x = 1, y = 0 }, 1314 | { x = 2, y = 0 }, 1315 | } 1316 | 1317 | local offsetsStaggerY = { 1318 | { x = 0, y = 0 }, 1319 | { x = -1, y = 1 }, 1320 | { x = 0, y = 1 }, 1321 | { x = 0, y = 2 }, 1322 | } 1323 | 1324 | local offsets = staggerX and offsetsStaggerX or offsetsStaggerY 1325 | 1326 | return 1327 | referenceX + offsets[nearest].x, 1328 | referenceY + offsets[nearest].y 1329 | end 1330 | end 1331 | 1332 | --- A list of individual layers indexed both by draw order and name 1333 | -- @table Map.layers 1334 | -- @see TileLayer 1335 | -- @see ObjectLayer 1336 | -- @see ImageLayer 1337 | -- @see CustomLayer 1338 | 1339 | --- A list of individual tiles indexed by Global ID 1340 | -- @table Map.tiles 1341 | -- @see Tile 1342 | -- @see Map.tileInstances 1343 | 1344 | --- A list of tile instances indexed by Global ID 1345 | -- @table Map.tileInstances 1346 | -- @see TileInstance 1347 | -- @see Tile 1348 | -- @see Map.tiles 1349 | 1350 | --- A list of no-longer-used batch sprites, indexed by batch 1351 | --@table Map.freeBatchSprites 1352 | 1353 | --- A list of individual objects indexed by Global ID 1354 | -- @table Map.objects 1355 | -- @see Object 1356 | 1357 | --- @table TileLayer 1358 | -- @field name The name of the layer 1359 | -- @field x Position on the X axis (in pixels) 1360 | -- @field y Position on the Y axis (in pixels) 1361 | -- @field width Width of layer (in tiles) 1362 | -- @field height Height of layer (in tiles) 1363 | -- @field visible Toggle if layer is visible or hidden 1364 | -- @field opacity Opacity of layer 1365 | -- @field properties Custom properties 1366 | -- @field data A tileWo dimensional table filled with individual tiles indexed by [y][x] (in tiles) 1367 | -- @field update Update function 1368 | -- @field draw Draw function 1369 | -- @see Map.layers 1370 | -- @see Tile 1371 | 1372 | --- @table ObjectLayer 1373 | -- @field name The name of the layer 1374 | -- @field x Position on the X axis (in pixels) 1375 | -- @field y Position on the Y axis (in pixels) 1376 | -- @field visible Toggle if layer is visible or hidden 1377 | -- @field opacity Opacity of layer 1378 | -- @field properties Custom properties 1379 | -- @field objects List of objects indexed by draw order 1380 | -- @field update Update function 1381 | -- @field draw Draw function 1382 | -- @see Map.layers 1383 | -- @see Object 1384 | 1385 | --- @table ImageLayer 1386 | -- @field name The name of the layer 1387 | -- @field x Position on the X axis (in pixels) 1388 | -- @field y Position on the Y axis (in pixels) 1389 | -- @field visible Toggle if layer is visible or hidden 1390 | -- @field opacity Opacity of layer 1391 | -- @field properties Custom properties 1392 | -- @field image Image to be drawn 1393 | -- @field update Update function 1394 | -- @field draw Draw function 1395 | -- @see Map.layers 1396 | 1397 | --- Custom Layers are used to place userdata such as sprites within the draw order of the map. 1398 | -- @table CustomLayer 1399 | -- @field name The name of the layer 1400 | -- @field x Position on the X axis (in pixels) 1401 | -- @field y Position on the Y axis (in pixels) 1402 | -- @field visible Toggle if layer is visible or hidden 1403 | -- @field opacity Opacity of layer 1404 | -- @field properties Custom properties 1405 | -- @field update Update function 1406 | -- @field draw Draw function 1407 | -- @see Map.layers 1408 | -- @usage 1409 | -- -- Create a Custom Layer 1410 | -- local spriteLayer = map:addCustomLayer("Sprite Layer", 3) 1411 | -- 1412 | -- -- Add data to Custom Layer 1413 | -- spriteLayer.sprites = { 1414 | -- player = { 1415 | -- image = lg.newImage("assets/sprites/player.png"), 1416 | -- x = 64, 1417 | -- y = 64, 1418 | -- r = 0, 1419 | -- } 1420 | -- } 1421 | -- 1422 | -- -- Update callback for Custom Layer 1423 | -- function spriteLayer:update(dt) 1424 | -- for _, sprite in pairs(self.sprites) do 1425 | -- sprite.r = sprite.r + math.rad(90 * dt) 1426 | -- end 1427 | -- end 1428 | -- 1429 | -- -- Draw callback for Custom Layer 1430 | -- function spriteLayer:draw() 1431 | -- for _, sprite in pairs(self.sprites) do 1432 | -- local x = math.floor(sprite.x) 1433 | -- local y = math.floor(sprite.y) 1434 | -- local r = sprite.r 1435 | -- lg.draw(sprite.image, x, y, r) 1436 | -- end 1437 | -- end 1438 | 1439 | --- @table Tile 1440 | -- @field id Local ID within Tileset 1441 | -- @field gid Global ID 1442 | -- @field tileset Tileset ID 1443 | -- @field quad Quad object 1444 | -- @field properties Custom properties 1445 | -- @field terrain Terrain data 1446 | -- @field animation Animation data 1447 | -- @field frame Current animation frame 1448 | -- @field time Time spent on current animation frame 1449 | -- @field width Width of tile 1450 | -- @field height Height of tile 1451 | -- @field sx Scale value on the X axis 1452 | -- @field sy Scale value on the Y axis 1453 | -- @field r Rotation of tile (in radians) 1454 | -- @field offset Offset drawing position 1455 | -- @field offset.x Offset value on the X axis 1456 | -- @field offset.y Offset value on the Y axis 1457 | -- @see Map.tiles 1458 | 1459 | --- @table TileInstance 1460 | -- @field batch Spritebatch the Tile Instance belongs to 1461 | -- @field id ID within the spritebatch 1462 | -- @field gid Global ID 1463 | -- @field x Position on the X axis (in pixels) 1464 | -- @field y Position on the Y axis (in pixels) 1465 | -- @see Map.tileInstances 1466 | -- @see Tile 1467 | 1468 | --- @table Object 1469 | -- @field id Global ID 1470 | -- @field name Name of object (non-unique) 1471 | -- @field shape Shape of object 1472 | -- @field x Position of object on X axis (in pixels) 1473 | -- @field y Position of object on Y axis (in pixels) 1474 | -- @field width Width of object (in pixels) 1475 | -- @field height Heigh tof object (in pixels) 1476 | -- @field rotation Rotation of object (in radians) 1477 | -- @field visible Toggle if object is visible or hidden 1478 | -- @field properties Custom properties 1479 | -- @field ellipse List of verticies of specific shape 1480 | -- @field rectangle List of verticies of specific shape 1481 | -- @field polygon List of verticies of specific shape 1482 | -- @field polyline List of verticies of specific shape 1483 | -- @see Map.objects 1484 | 1485 | return setmetatable({}, STI) 1486 | --------------------------------------------------------------------------------