diff --git a/Game/GameKeyPressed.lua b/Game/GameKeyPressed.lua index bc633df..e23025c 100644 --- a/Game/GameKeyPressed.lua +++ b/Game/GameKeyPressed.lua @@ -1,8 +1,7 @@ function GameKeyPressed(key) - if key == "escape" then - musicBattle:setVolume(0) - musicPause:setVolume(0.5) + musicBattle:setVolume(0) + musicPause:setVolume(0.6) _G.GAMESTATE = "PAUSE" print("STATE CHANEGD: PAUSED!") @@ -13,13 +12,8 @@ function GameKeyPressed(key) DebugFlag = not DebugFlag end - --TODO: Move player movement code into here! - - - --[[ - -- TODO: Better restart + --TODO: Better restart if key == "r" and not _G.PAUSED then love.load() end - ]]-- end diff --git a/Game/UpdateGame.lua b/Game/UpdateGame.lua index 76aa05c..26b32b5 100644 --- a/Game/UpdateGame.lua +++ b/Game/UpdateGame.lua @@ -1,9 +1,31 @@ +local function checkWinState() + -- Check P1's health + if UserPlayer1.health <= 0 then --P1 win + _G.P1_WIN = true + _G.P2_WIN = false + UserPlayer1.health = 0 + return true + end + if UserPlayer2.health <= 0 then --P2 win + _G.P1_WIN = false + _G.P2_WIN = true + UserPlayer2.health = 0 + return true + end + return false +end + +local max = math.max -- optimisations + function UpdateGame(dt) + --Check if anyone has won + if checkWinState() == true then + print("STATE CHNAGED: WIN!") + _G.GAMESTATE = "WIN" + end --WindField World:update(dt) - local max = math.max - KeyPressTime1 = max(0, KeyPressTime1 - dt) if KeyPressTime1 <= 0 then EnableKeyPress1 = true @@ -13,6 +35,7 @@ function UpdateGame(dt) if KeyPressTime2 <= 0 then EnableKeyPress2 = true end + for i, v in ipairs(Bullets1) do v:update(dt) if v.y < 0 then --top of screen @@ -69,7 +92,10 @@ function UpdateGame(dt) end end end - + UserPlayer1:handleKeys("w", "s", "a", "d", dt) + UserPlayer2:handleKeys("up", "down", "left", "right", dt) + UserPlayer1:updateCol() + UserPlayer2:updateCol() UserPlayer1:update(dt) UserPlayer2:update(dt) end diff --git a/Menu/DrawMenu.lua b/Menu/DrawMenu.lua index 9101ed3..0000179 100644 --- a/Menu/DrawMenu.lua +++ b/Menu/DrawMenu.lua @@ -1,44 +1,43 @@ -local function button(x,y, w, h, text, selected) - --x,y is the top left corner of the button - local rounding = 30 -- used for rounding the buttons +local function button(x, y, w, h, text, selected) + --x,y is the top left corner of the button + local rounding = 30 -- used for rounding the buttons - if not selected then - love.graphics.setColor(love.math.colorFromBytes(41,134,204)) - elseif selected then - love.graphics.setColor(love.math.colorFromBytes(244,67,54)) - end - -- Draw rectangle - love.graphics.rectangle("line", x, y, w, h, rounding, rounding) + if not selected then + love.graphics.setColor(love.math.colorFromBytes(41, 134, 204)) + elseif selected then + love.graphics.setColor(love.math.colorFromBytes(244, 67, 54)) + end + -- Draw rectangle + love.graphics.rectangle("line", x, y, w, h, rounding, rounding) - -- Get width and height of text - local tw = MenuFont:getWidth(text) - local th = MenuFont:getHeight(text) - -- Calculate position to center the text - local textX = x + (w - tw) / 2 - local textY = y + (h - th) / 2 - -- Place text inside the rectangle + -- Get width and height of text + local tw = MenuFont:getWidth(text) + local th = MenuFont:getHeight(text) + -- Calculate position to center the text + local textX = x + (w - tw) / 2 + local textY = y + (h - th) / 2 + -- Place text inside the rectangle love.graphics.setFont(MenuFont) - love.graphics.print(text, textX, textY) + love.graphics.print(text, textX, textY) end local function title() local height = love.graphics.getHeight() local width = love.graphics.getWidth() love.graphics.setFont(GameFont) - love.graphics.setColor(0.5,1,1) - love.graphics.rectangle("fill", 0, 0, width, height) - love.graphics.setColor(0,0,0) - love.graphics.print("MENU", 100,100) + love.graphics.setColor(0.5, 1, 1) + love.graphics.rectangle("fill", 0, 0, width, height) + love.graphics.setColor(0, 0, 0) + love.graphics.print("MENU", 100, 100) end - function DrawMenu() - local bwidth, bheight = 300, 140 - title() - button(100, 200, bwidth, bheight, "Play", MENU_POS == 0 and true or false) - button(100, 350, bwidth, bheight, "???", MENU_POS == 1 and true or false) - button(100, 500, bwidth, bheight, "???", MENU_POS == 2 and true or false) - button(100, 650, bwidth, bheight, "Quit", MENU_POS == 3 and true or false) + local bwidth, bheight = 300, 140 + title() + button(100, 200, bwidth, bheight, "Play", MENU_POS == 0 and true or false) + button(100, 350, bwidth, bheight, "???", MENU_POS == 1 and true or false) + button(100, 500, bwidth, bheight, "???", MENU_POS == 2 and true or false) + button(100, 650, bwidth, bheight, "Quit", MENU_POS == 3 and true or false) - love.graphics.setColor(255,255,255) -- reset colours -end \ No newline at end of file + love.graphics.setColor(255, 255, 255) -- reset colours +end diff --git a/Menu/MenuKeyPressed.lua b/Menu/MenuKeyPressed.lua index f3601f8..c64853c 100644 --- a/Menu/MenuKeyPressed.lua +++ b/Menu/MenuKeyPressed.lua @@ -1,5 +1,5 @@ function MenuKeyPressed(key) - if key == 'return' then + if key == "return" then -- 0 Start Game -- 1 ?? -- 2 ??? @@ -9,17 +9,13 @@ function MenuKeyPressed(key) _G.GAMESTATE = "GAME" print("STATE CHANEGD: GAME!") musicMenu:stop() - elseif MENU_POS == 1 then print("STATE CHANEGD: DUNNO!") - elseif MENU_POS == 2 then print("STATE CHANEGD: DUNNO!") - elseif MENU_POS == 3 then love.event.quit() end - end if love.keyboard.isDown("up") then @@ -37,5 +33,4 @@ function MenuKeyPressed(key) _G.MENU_POS = _G.MENU_POS + 1 end end - end diff --git a/Menu/UpdateMenu.lua b/Menu/UpdateMenu.lua index aa993fb..95bd3c8 100644 --- a/Menu/UpdateMenu.lua +++ b/Menu/UpdateMenu.lua @@ -1,2 +1 @@ -function UpdateMenu(dt) -end \ No newline at end of file +function UpdateMenu(dt) end diff --git a/Pause/DrawPause.lua b/Pause/DrawPause.lua index ac799f0..c1e6b6b 100644 --- a/Pause/DrawPause.lua +++ b/Pause/DrawPause.lua @@ -1,45 +1,44 @@ -local function button(x,y, w, h, text, selected) - --x,y is the top left corner of the button - local rounding = 30 -- used for rounding the buttons +local function button(x, y, w, h, text, selected) + --x,y is the top left corner of the button + local rounding = 30 -- used for rounding the buttons - if not selected then - love.graphics.setColor(love.math.colorFromBytes(41,134,204)) - elseif selected then - love.graphics.setColor(love.math.colorFromBytes(244,67,54)) - end - -- Draw rectangle - love.graphics.rectangle("fill", x, y, w, h, rounding, rounding) + if not selected then + love.graphics.setColor(love.math.colorFromBytes(41, 134, 204)) + elseif selected then + love.graphics.setColor(love.math.colorFromBytes(244, 67, 54)) + end + -- Draw rectangle + love.graphics.rectangle("fill", x, y, w, h, rounding, rounding) - -- Get width and height of text - local tw = MenuFont:getWidth(text) - local th = MenuFont:getHeight(text) - -- Calculate position to center the text - local textX = x + (w - tw) / 2 - local textY = y + (h - th) / 2 - -- Place text inside the rectangle + -- Get width and height of text + local tw = MenuFont:getWidth(text) + local th = MenuFont:getHeight(text) + -- Calculate position to center the text + local textX = x + (w - tw) / 2 + local textY = y + (h - th) / 2 + -- Place text inside the rectangle love.graphics.setFont(MenuFont) - love.graphics.setColor(1,1,1) -- reset colours - love.graphics.print(text, textX, textY) + love.graphics.setColor(1, 1, 1) -- reset colours + love.graphics.print(text, textX, textY) end - function DrawPause() - local opacity = 0.3 + local opacity = 0.3 local height = love.graphics.getHeight() local width = love.graphics.getWidth() - local bwidth, bheight = 300, 140 + local bwidth, bheight = 300, 140 love.graphics.setFont(GameFont) - DrawGame() --Draw a single frame of the game - love.graphics.setColor(0.1,0.1,0.1, opacity) --overlay opaque img - love.graphics.rectangle("fill", 0, 0, width, height) + DrawGame() --Draw a single frame of the game + love.graphics.setColor(0.1, 0.1, 0.1, opacity) --overlay opaque img + love.graphics.rectangle("fill", 0, 0, width, height) - love.graphics.setColor(1,1,1) - love.graphics.print("PAUSED", 100,100) - --love.graphics.print("" .. PAUSE_POS, 200,200) + love.graphics.setColor(1, 1, 1) + love.graphics.print("PAUSED", 100, 100) + --love.graphics.print("" .. PAUSE_POS, 200,200) - button(100, 200, bwidth, bheight, "Return", PAUSE_POS == 0 and true or false) - button(100, 350, bwidth, bheight, "Menu", PAUSE_POS == 1 and true or false) - button(100, 500, bwidth, bheight, "Quit", PAUSE_POS == 2 and true or false) - love.graphics.setColor(255,255,255) -- reset colours -end \ No newline at end of file + button(100, 200, bwidth, bheight, "Return", PAUSE_POS == 0 and true or false) + button(100, 350, bwidth, bheight, "Menu", PAUSE_POS == 1 and true or false) + button(100, 500, bwidth, bheight, "Quit", PAUSE_POS == 2 and true or false) + love.graphics.setColor(255, 255, 255) -- reset colours +end diff --git a/Pause/PauseKeyPressed.lua b/Pause/PauseKeyPressed.lua index 28dfd55..3c98fe5 100644 --- a/Pause/PauseKeyPressed.lua +++ b/Pause/PauseKeyPressed.lua @@ -1,30 +1,26 @@ function PauseKeyPressed(key) - if key == 'return' then - musicBattle:setVolume(0.5) - musicPause:setVolume(0) - -- 0 Return to game - -- 1 Quit - if PAUSE_POS == 0 then - -- unpause the game - _G.GAMESTATE = "GAME" - print("STATE CHANEGD: GAME!") - _G.PAUSED = false + if key == "return" then + musicBattle:setVolume(0.5) + musicPause:setVolume(0) + -- 0 Return to game + -- 1 Quit + if PAUSE_POS == 0 then + -- unpause the game + _G.GAMESTATE = "GAME" + print("STATE CHANEGD: GAME!") + _G.PAUSED = false musicBattle:setVolume(0.5) musicPause:setVolume(0) - - elseif PAUSE_POS == 1 then - _G.GAMESTATE = "MENU" - print("STATE CHANEGD: MENU!") - _G.PAUSED = false - musicPause:stop() - musicBattle:stop() - - elseif PAUSE_POS == 2 then + elseif PAUSE_POS == 1 then + _G.GAMESTATE = "MENU" + print("STATE CHANEGD: MENU!") + _G.PAUSED = false + musicPause:stop() + musicBattle:stop() + elseif PAUSE_POS == 2 then love.event.quit() - end - - end - + end + end if love.keyboard.isDown("up") then if _G.PAUSE_POS == 0 then @@ -41,4 +37,4 @@ function PauseKeyPressed(key) _G.PAUSE_POS = _G.PAUSE_POS + 1 end end -end \ No newline at end of file +end diff --git a/Pause/UpdatePause.lua b/Pause/UpdatePause.lua index 2a2cc04..dd7c4cb 100644 --- a/Pause/UpdatePause.lua +++ b/Pause/UpdatePause.lua @@ -1,2 +1 @@ -function UpdatePause(dt) -end \ No newline at end of file +function UpdatePause(dt) end diff --git a/constants.lua b/constants.lua index d31faef..0f4ec2b 100644 --- a/constants.lua +++ b/constants.lua @@ -1,3 +1,11 @@ +--[[ +* Game states: +* - MENU +* - GAME +* - PAUSE +* - WIN +]] +-- GAMESTATE = "MENU" MENU_POS = 0 @@ -5,4 +13,8 @@ MENU_MAX = 3 --0 play, 1 ?, 2 ?, 3 quit PAUSED = false PAUSE_POS = 0 -PAUSE_MAX = 2 -- 0 resume, 1 menu, 2 quit \ No newline at end of file +PAUSE_MAX = 2 -- 0 resume, 1 menu, 2 quit + +-- WIN flags for P1 and P2 +P1_WIN = false +P2_WIN = false diff --git a/game.love b/game.love index 3f38fce..4ced93b 100644 Binary files a/game.love and b/game.love differ diff --git a/libs/classic.lua b/libs/classic.lua index cbd6f81..035bbff 100644 --- a/libs/classic.lua +++ b/libs/classic.lua @@ -7,62 +7,53 @@ -- the terms of the MIT license. See LICENSE for details. -- - local Object = {} Object.__index = Object - -function Object:new() -end - +function Object:new() end function Object:extend() - local cls = {} - for k, v in pairs(self) do - if k:find("__") == 1 then - cls[k] = v - end - end - cls.__index = cls - cls.super = self - setmetatable(cls, self) - return cls + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls end - function Object:implement(...) - for _, cls in pairs({...}) do - for k, v in pairs(cls) do - if self[k] == nil and type(v) == "function" then - self[k] = v - end - end - end + for _, cls in pairs({ ... }) do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == "function" then + self[k] = v + end + end + end end - function Object:is(T) - local mt = getmetatable(self) - while mt do - if mt == T then - return true - end - mt = getmetatable(mt) - end - return false + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false end - function Object:__tostring() - return "Object" + return "Object" end - function Object:__call(...) - local obj = setmetatable({}, self) - obj:new(...) - return obj + local obj = setmetatable({}, self) + obj:new(...) + return obj end - return Object diff --git a/libs/profile.lua b/libs/profile.lua index c42d266..dc8bcc0 100644 --- a/libs/profile.lua +++ b/libs/profile.lua @@ -23,91 +23,91 @@ local _internal = {} -- @tparam number line Line number -- @tparam[opt] table info Debug info table function profile.hooker(event, line, info) - info = info or debug.getinfo(2, 'fnS') - local f = info.func - -- ignore the profiler itself - if _internal[f] or info.what ~= "Lua" then - return - end - -- get the function name if available - if info.name then - _labeled[f] = info.name - end - -- find the line definition - if not _defined[f] then - _defined[f] = info.short_src..":"..info.linedefined - _ncalls[f] = 0 - _telapsed[f] = 0 - end - if _tcalled[f] then - local dt = clock() - _tcalled[f] - _telapsed[f] = _telapsed[f] + dt - _tcalled[f] = nil - end - if event == "tail call" then - local prev = debug.getinfo(3, 'fnS') - profile.hooker("return", line, prev) - profile.hooker("call", line, info) - elseif event == 'call' then - _tcalled[f] = clock() - else - _ncalls[f] = _ncalls[f] + 1 - end + info = info or debug.getinfo(2, "fnS") + local f = info.func + -- ignore the profiler itself + if _internal[f] or info.what ~= "Lua" then + return + end + -- get the function name if available + if info.name then + _labeled[f] = info.name + end + -- find the line definition + if not _defined[f] then + _defined[f] = info.short_src .. ":" .. info.linedefined + _ncalls[f] = 0 + _telapsed[f] = 0 + end + if _tcalled[f] then + local dt = clock() - _tcalled[f] + _telapsed[f] = _telapsed[f] + dt + _tcalled[f] = nil + end + if event == "tail call" then + local prev = debug.getinfo(3, "fnS") + profile.hooker("return", line, prev) + profile.hooker("call", line, info) + elseif event == "call" then + _tcalled[f] = clock() + else + _ncalls[f] = _ncalls[f] + 1 + end end --- Sets a clock function to be used by the profiler. -- @tparam function func Clock function that returns a number function profile.setclock(f) - assert(type(f) == "function", "clock must be a function") - clock = f + assert(type(f) == "function", "clock must be a function") + clock = f end --- Starts collecting data. function profile.start() - if rawget(_G, 'jit') then - jit.off() - jit.flush() - end - debug.sethook(profile.hooker, "cr") + if rawget(_G, "jit") then + jit.off() + jit.flush() + end + debug.sethook(profile.hooker, "cr") end --- Stops collecting data. function profile.stop() - debug.sethook() - for f in pairs(_tcalled) do - local dt = clock() - _tcalled[f] - _telapsed[f] = _telapsed[f] + dt - _tcalled[f] = nil - end - -- merge closures - local lookup = {} - for f, d in pairs(_defined) do - local id = (_labeled[f] or '?')..d - local f2 = lookup[id] - if f2 then - _ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0) - _telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0) - _defined[f], _labeled[f] = nil, nil - _ncalls[f], _telapsed[f] = nil, nil - else - lookup[id] = f - end - end - collectgarbage('collect') + debug.sethook() + for f in pairs(_tcalled) do + local dt = clock() - _tcalled[f] + _telapsed[f] = _telapsed[f] + dt + _tcalled[f] = nil + end + -- merge closures + local lookup = {} + for f, d in pairs(_defined) do + local id = (_labeled[f] or "?") .. d + local f2 = lookup[id] + if f2 then + _ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0) + _telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0) + _defined[f], _labeled[f] = nil, nil + _ncalls[f], _telapsed[f] = nil, nil + else + lookup[id] = f + end + end + collectgarbage("collect") end --- Resets all collected data. function profile.reset() - for f in pairs(_ncalls) do - _ncalls[f] = 0 - end - for f in pairs(_telapsed) do - _telapsed[f] = 0 - end - for f in pairs(_tcalled) do - _tcalled[f] = nil - end - collectgarbage('collect') + for f in pairs(_ncalls) do + _ncalls[f] = 0 + end + for f in pairs(_telapsed) do + _telapsed[f] = 0 + end + for f in pairs(_tcalled) do + _tcalled[f] = nil + end + collectgarbage("collect") end --- This is an internal function. @@ -115,11 +115,11 @@ end -- @tparam function b Second function -- @treturn boolean True if "a" should rank higher than "b" function profile.comp(a, b) - local dt = _telapsed[b] - _telapsed[a] - if dt == 0 then - return _ncalls[b] < _ncalls[a] - end - return dt < 0 + local dt = _telapsed[b] - _telapsed[a] + if dt == 0 then + return _ncalls[b] < _ncalls[a] + end + return dt < 0 end --- Generates a report of functions that have been called since the profile was started. @@ -127,26 +127,26 @@ end -- @tparam[opt] number limit Maximum number of rows -- @treturn table Table of rows function profile.query(limit) - local t = {} - for f, n in pairs(_ncalls) do - if n > 0 then - t[#t + 1] = f - end - end - table.sort(t, profile.comp) - if limit then - while #t > limit do - table.remove(t) - end - end - for i, f in ipairs(t) do - local dt = 0 - if _tcalled[f] then - dt = clock() - _tcalled[f] - end - t[i] = { i, _labeled[f] or '?', _ncalls[f], _telapsed[f] + dt, _defined[f] } - end - return t + local t = {} + for f, n in pairs(_ncalls) do + if n > 0 then + t[#t + 1] = f + end + end + table.sort(t, profile.comp) + if limit then + while #t > limit do + table.remove(t) + end + end + for i, f in ipairs(t) do + local dt = 0 + if _tcalled[f] then + dt = clock() - _tcalled[f] + end + t[i] = { i, _labeled[f] or "?", _ncalls[f], _telapsed[f] + dt, _defined[f] } + end + return t end local cols = { 3, 29, 11, 24, 32 } @@ -156,38 +156,40 @@ local cols = { 3, 29, 11, 24, 32 } -- @tparam[opt] number limit Maximum number of rows -- @treturn string Text-based profiling report function profile.report(n) - local out = {} - local report = profile.query(n) - for i, row in ipairs(report) do - for j = 1, 5 do - local s = row[j] - local l2 = cols[j] - s = tostring(s) - local l1 = s:len() - if l1 < l2 then - s = s..(' '):rep(l2-l1) - elseif l1 > l2 then - s = s:sub(l1 - l2 + 1, l1) - end - row[j] = s - end - out[i] = table.concat(row, ' | ') - end + local out = {} + local report = profile.query(n) + for i, row in ipairs(report) do + for j = 1, 5 do + local s = row[j] + local l2 = cols[j] + s = tostring(s) + local l1 = s:len() + if l1 < l2 then + s = s .. (" "):rep(l2 - l1) + elseif l1 > l2 then + s = s:sub(l1 - l2 + 1, l1) + end + row[j] = s + end + out[i] = table.concat(row, " | ") + end - local row = " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n" - local col = " | # | Function | Calls | Time | Code | \n" - local sz = row..col..row - if #out > 0 then - sz = sz..' | '..table.concat(out, ' | \n | ')..' | \n' - end - return '\n'..sz..row + local row = + " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n" + local col = + " | # | Function | Calls | Time | Code | \n" + local sz = row .. col .. row + if #out > 0 then + sz = sz .. " | " .. table.concat(out, " | \n | ") .. " | \n" + end + return "\n" .. sz .. row end -- store all internal profiler functions for _, v in pairs(profile) do - if type(v) == "function" then - _internal[v] = true - end + if type(v) == "function" then + _internal[v] = true + end end -return profile \ No newline at end of file +return profile diff --git a/libs/sti/atlas.lua b/libs/sti/atlas.lua index 302b332..bd710fd 100644 --- a/libs/sti/atlas.lua +++ b/libs/sti/atlas.lua @@ -10,150 +10,179 @@ local module = {} -- @param sort If "size" will sort by size, or if "id" will sort by id -- @param ids Array with ids of each file -- @param pow2 If true, will force a power of 2 size -function module.Atlas( files, sort, ids, pow2 ) - - local function Node(x, y, w, h) - return {x = x, y = y, w = w, h = h} - end - - local function nextpow2( n ) - local res = 1 - while res <= n do - res = res * 2 - end - return res - end - - local function loadImgs() - local images = {} - for i = 1, #files do - images[i] = {} - --images[i].name = files[i] - if ids then images[i].id = ids[i] end - images[i].img = love.graphics.newImage( files[i] ) - images[i].w = images[i].img:getWidth() - images[i].h = images[i].img:getHeight() - images[i].area = images[i].w * images[i].h - end - if sort == "size" or sort == "id" then - table.sort( images, function( a, b ) return ( a.area > b.area ) end ) - end - return images - end - - --TODO: understand this func - local function add(root, id, w, h) - if root.left or root.right then - if root.left then - local node = add(root.left, id, w, h) - if node then return node end - end - if root.right then - local node = add(root.right, id, w, h) - if node then return node end - end - return nil - end - - if w > root.w or h > root.h then return nil end - - local _w, _h = root.w - w, root.h - h - - if _w <= _h then - root.left = Node(root.x + w, root.y, _w, h) - root.right = Node(root.x, root.y + h, root.w, _h) - else - root.left = Node(root.x, root.y + h, w, _h) - root.right = Node(root.x + w, root.y, _w, root.h) - end - - root.w = w - root.h = h - root.id = id - - return root - end - - local function unmap(root) - if not root then return {} end - - local tree = {} - if root.id then - tree[root.id] = {} - tree[root.id].x, tree[root.id].y = root.x, root.y - end - - local left = unmap(root.left) - local right = unmap(root.right) - - for k, v in pairs(left) do - tree[k] = {} - tree[k].x, tree[k].y = v.x, v.y - end - for k, v in pairs(right) do - tree[k] = {} - tree[k].x, tree[k].y = v.x, v.y - end - - return tree - end - - local function bake() - local images = loadImgs() - - local root = {} - local w, h = images[1].w, images[1].h - - if pow2 then - if w % 1 == 0 then w = nextpow2(w) end - if h % 1 == 0 then h = nextpow2(h) end - end - - repeat - local node - - root = Node(0, 0, w, h) - - for i = 1, #images do - node = add(root, i, images[i].w, images[i].h) - if not node then break end - end - - if not node then - if h <= w then - if pow2 then h = h * 2 else h = h + 1 end - else - if pow2 then w = w * 2 else w = w + 1 end - end - else - break - end - until false - - local limits = love.graphics.getSystemLimits() - if w > limits.texturesize or h > limits.texturesize then - return "Resulting texture is too large for this system" - end - - local coords = unmap(root) - local map = love.graphics.newCanvas(w, h) - love.graphics.setCanvas( map ) --- love.graphics.clear() - - for i = 1, #images do - love.graphics.draw(images[i].img, coords[i].x, coords[i].y) - if ids then coords[i].id = images[i].id end - end - love.graphics.setCanvas() - - if sort == "ids" then - table.sort( coords, function( a, b ) return ( a.id < b.id ) end ) - end - - return { image = map, coords = coords } - end - - return bake() +function module.Atlas(files, sort, ids, pow2) + local function Node(x, y, w, h) + return { x = x, y = y, w = w, h = h } + end + + local function nextpow2(n) + local res = 1 + while res <= n do + res = res * 2 + end + return res + end + + local function loadImgs() + local images = {} + for i = 1, #files do + images[i] = {} + --images[i].name = files[i] + if ids then + images[i].id = ids[i] + end + images[i].img = love.graphics.newImage(files[i]) + images[i].w = images[i].img:getWidth() + images[i].h = images[i].img:getHeight() + images[i].area = images[i].w * images[i].h + end + if sort == "size" or sort == "id" then + table.sort(images, function(a, b) + return (a.area > b.area) + end) + end + return images + end + + --TODO: understand this func + local function add(root, id, w, h) + if root.left or root.right then + if root.left then + local node = add(root.left, id, w, h) + if node then + return node + end + end + if root.right then + local node = add(root.right, id, w, h) + if node then + return node + end + end + return nil + end + + if w > root.w or h > root.h then + return nil + end + + local _w, _h = root.w - w, root.h - h + + if _w <= _h then + root.left = Node(root.x + w, root.y, _w, h) + root.right = Node(root.x, root.y + h, root.w, _h) + else + root.left = Node(root.x, root.y + h, w, _h) + root.right = Node(root.x + w, root.y, _w, root.h) + end + + root.w = w + root.h = h + root.id = id + + return root + end + + local function unmap(root) + if not root then + return {} + end + + local tree = {} + if root.id then + tree[root.id] = {} + tree[root.id].x, tree[root.id].y = root.x, root.y + end + + local left = unmap(root.left) + local right = unmap(root.right) + + for k, v in pairs(left) do + tree[k] = {} + tree[k].x, tree[k].y = v.x, v.y + end + for k, v in pairs(right) do + tree[k] = {} + tree[k].x, tree[k].y = v.x, v.y + end + + return tree + end + + local function bake() + local images = loadImgs() + + local root = {} + local w, h = images[1].w, images[1].h + + if pow2 then + if w % 1 == 0 then + w = nextpow2(w) + end + if h % 1 == 0 then + h = nextpow2(h) + end + end + + repeat + local node + + root = Node(0, 0, w, h) + + for i = 1, #images do + node = add(root, i, images[i].w, images[i].h) + if not node then + break + end + end + + if not node then + if h <= w then + if pow2 then + h = h * 2 + else + h = h + 1 + end + else + if pow2 then + w = w * 2 + else + w = w + 1 + end + end + else + break + end + until false + + local limits = love.graphics.getSystemLimits() + if w > limits.texturesize or h > limits.texturesize then + return "Resulting texture is too large for this system" + end + + local coords = unmap(root) + local map = love.graphics.newCanvas(w, h) + love.graphics.setCanvas(map) + -- love.graphics.clear() + + for i = 1, #images do + love.graphics.draw(images[i].img, coords[i].x, coords[i].y) + if ids then + coords[i].id = images[i].id + end + end + love.graphics.setCanvas() + + if sort == "ids" then + table.sort(coords, function(a, b) + return (a.id < b.id) + end) + end + + return { image = map, coords = coords } + end + + return bake() end return module diff --git a/libs/sti/graphics.lua b/libs/sti/graphics.lua index 6acf8d6..5ad6efd 100644 --- a/libs/sti/graphics.lua +++ b/libs/sti/graphics.lua @@ -1,4 +1,4 @@ -local lg = _G.love.graphics +local lg = _G.love.graphics local graphics = { isCreated = lg and true or false } function graphics.newSpriteBatch(...) diff --git a/libs/sti/init.lua b/libs/sti/init.lua index 646a224..535468e 100644 --- a/libs/sti/init.lua +++ b/libs/sti/init.lua @@ -5,22 +5,22 @@ -- @license MIT/X11 local STI = { - _LICENSE = "MIT/X11", - _URL = "https://github.com/karai17/Simple-Tiled-Implementation", - _VERSION = "1.2.3.0", + _LICENSE = "MIT/X11", + _URL = "https://github.com/karai17/Simple-Tiled-Implementation", + _VERSION = "1.2.3.0", _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.", - cache = {} + cache = {}, } STI.__index = STI -local love = _G.love -local cwd = (...):gsub('%.init$', '') .. "." +local love = _G.love +local cwd = (...):gsub("%.init$", "") .. "." local utils = require(cwd .. "utils") -local ceil = math.ceil +local ceil = math.ceil local floor = math.floor -local lg = require(cwd .. "graphics") +local lg = require(cwd .. "graphics") local atlas = require(cwd .. "atlas") -local Map = {} +local Map = {} Map.__index = Map local function new(map, plugins, ox, oy) @@ -31,10 +31,7 @@ local function new(map, plugins, ox, oy) else -- Check for valid map type local ext = map:sub(-4, -1) - assert(ext == ".lua", string.format( - "Invalid file type: %s. File must be of type: lua.", - ext - )) + assert(ext == ".lua", string.format("Invalid file type: %s. File must be of type: lua.", ext)) -- Get directory of map dir = map:reverse():find("[/\\]") or "" @@ -79,58 +76,58 @@ function Map:init(path, plugins, ox, oy) end self:resize() - self.objects = {} - self.tiles = {} + self.objects = {} + self.tiles = {} self.tileInstances = {} self.offsetx = ox or 0 self.offsety = oy or 0 self.freeBatchSprites = {} - setmetatable(self.freeBatchSprites, { __mode = 'k' }) + setmetatable(self.freeBatchSprites, { __mode = "k" }) -- Set tiles, images local gid = 1 for i, tileset in ipairs(self.tilesets) do assert(not tileset.filename, "STI does not support external Tilesets.\nYou need to embed all Tilesets.") - if tileset.image then - -- Cache images - if lg.isCreated then - local formatted_path = utils.format_path(path .. tileset.image) - - if not STI.cache[formatted_path] then - utils.fix_transparent_color(tileset, formatted_path) - utils.cache_image(STI, formatted_path, tileset.image) - else - tileset.image = STI.cache[formatted_path] - end - end - - gid = self:setTiles(i, tileset, gid) - elseif tileset.tilecount > 0 then - -- Build atlas for image collection - local files, ids = {}, {} - for j = 1, #tileset.tiles do - files[ j ] = utils.format_path(path .. tileset.tiles[j].image) - ids[ j ] = tileset.tiles[j].id - end - - local map = atlas.Atlas( files, "ids", ids ) - - if lg.isCreated then - local formatted_path = utils.format_path(path .. tileset.name) - - if not STI.cache[formatted_path] then - -- No need to fix transparency color for collections - utils.cache_image(STI, formatted_path, map.image) - tileset.image = map.image - else - tileset.image = STI.cache[formatted_path] - end - end - - gid = self:setAtlasTiles(i, tileset, map.coords, gid) - end + if tileset.image then + -- Cache images + if lg.isCreated then + local formatted_path = utils.format_path(path .. tileset.image) + + if not STI.cache[formatted_path] then + utils.fix_transparent_color(tileset, formatted_path) + utils.cache_image(STI, formatted_path, tileset.image) + else + tileset.image = STI.cache[formatted_path] + end + end + + gid = self:setTiles(i, tileset, gid) + elseif tileset.tilecount > 0 then + -- Build atlas for image collection + local files, ids = {}, {} + for j = 1, #tileset.tiles do + files[j] = utils.format_path(path .. tileset.tiles[j].image) + ids[j] = tileset.tiles[j].id + end + + local map = atlas.Atlas(files, "ids", ids) + + if lg.isCreated then + local formatted_path = utils.format_path(path .. tileset.name) + + if not STI.cache[formatted_path] then + -- No need to fix transparency color for collections + utils.cache_image(STI, formatted_path, map.image) + tileset.image = map.image + else + tileset.image = STI.cache[formatted_path] + end + end + + gid = self:setAtlasTiles(i, tileset, map.coords, gid) + end end local layers = {} @@ -174,7 +171,7 @@ end -- @param plugins A list of plugins to load function Map:loadPlugins(plugins) for _, plugin in ipairs(plugins) do - local pluginModulePath = cwd .. 'plugins.' .. plugin + local pluginModulePath = cwd .. "plugins." .. plugin local ok, pluginModule = pcall(require, pluginModulePath) if ok then for k, func in pairs(pluginModule) do @@ -192,19 +189,19 @@ end -- @param gid First Global ID in Tileset -- @return number Next Tileset's first Global ID function Map:setTiles(index, tileset, gid) - local quad = lg.newQuad - local imageW = tileset.imagewidth - local imageH = tileset.imageheight - local tileW = tileset.tilewidth - local tileH = tileset.tileheight - local margin = tileset.margin + local quad = lg.newQuad + local imageW = tileset.imagewidth + local imageH = tileset.imageheight + local tileW = tileset.tilewidth + local tileH = tileset.tileheight + local margin = tileset.margin local spacing = tileset.spacing - local w = utils.get_tiles(imageW, tileW, margin, spacing) - local h = utils.get_tiles(imageH, tileH, margin, spacing) + local w = utils.get_tiles(imageW, tileW, margin, spacing) + local h = utils.get_tiles(imageH, tileH, margin, spacing) for y = 1, h do for x = 1, w do - local id = gid - tileset.firstgid + local id = gid - tileset.firstgid local quadX = (x - 1) * tileW + margin + (x - 1) * spacing local quadY = (y - 1) * tileH + margin + (y - 1) * spacing local type = "" @@ -212,10 +209,10 @@ function Map:setTiles(index, tileset, gid) for _, tile in pairs(tileset.tiles) do if tile.id == id then - properties = tile.properties - animation = tile.animation + properties = tile.properties + animation = tile.animation objectGroup = tile.objectGroup - type = tile.type + type = tile.type if tile.terrain then terrain = {} @@ -228,31 +225,27 @@ function Map:setTiles(index, tileset, gid) end local tile = { - id = id, - gid = gid, - tileset = index, - type = type, - quad = quad( - quadX, quadY, - tileW, tileH, - imageW, imageH - ), - properties = properties or {}, - terrain = terrain, - animation = animation, + id = id, + gid = gid, + tileset = index, + type = type, + quad = quad(quadX, quadY, tileW, tileH, imageW, imageH), + properties = properties or {}, + terrain = terrain, + animation = animation, objectGroup = objectGroup, - frame = 1, - time = 0, - width = tileW, - height = tileH, - sx = 1, - sy = 1, - r = 0, - offset = tileset.tileoffset, + frame = 1, + time = 0, + width = tileW, + height = tileH, + sx = 1, + sy = 1, + r = 0, + offset = tileset.tileoffset, } self.tiles[gid] = tile - gid = gid + 1 + gid = gid + 1 end end @@ -266,50 +259,46 @@ end -- @param gid First Global ID in Tileset -- @return number Next Tileset's first Global ID function Map:setAtlasTiles(index, tileset, coords, gid) - local quad = lg.newQuad - local imageW = tileset.image:getWidth() - local imageH = tileset.image:getHeight() - - local firstgid = tileset.firstgid - for i = 1, #tileset.tiles do - local tile = tileset.tiles[i] - if tile.terrain then - terrain = {} - - for j = 1, #tile.terrain do - terrain[j] = tileset.terrains[tile.terrain[j] + 1] - end - end - - local tile = { - id = tile.id, - gid = firstgid + tile.id, - tileset = index, - class = tile.class, - quad = quad( - coords[i].x, coords[i].y, - tile.width, tile.height, - imageW, imageH - ), - properties = tile.properties or {}, - terrain = terrain, - animation = tile.animation, - objectGroup = tile.objectGroup, - frame = 1, - time = 0, - width = tile.width, - height = tile.height, - sx = 1, - sy = 1, - r = 0, - offset = tileset.tileoffset, - } - - -- Be aware that in collections self.tiles can be a sparse array - self.tiles[tile.gid] = tile - end - - return gid + #tileset.tiles + local quad = lg.newQuad + local imageW = tileset.image:getWidth() + local imageH = tileset.image:getHeight() + + local firstgid = tileset.firstgid + for i = 1, #tileset.tiles do + local tile = tileset.tiles[i] + if tile.terrain then + terrain = {} + + for j = 1, #tile.terrain do + terrain[j] = tileset.terrains[tile.terrain[j] + 1] + end + end + + local tile = { + id = tile.id, + gid = firstgid + tile.id, + tileset = index, + class = tile.class, + quad = quad(coords[i].x, coords[i].y, tile.width, tile.height, imageW, imageH), + properties = tile.properties or {}, + terrain = terrain, + animation = tile.animation, + objectGroup = tile.objectGroup, + frame = 1, + time = 0, + width = tile.width, + height = tile.height, + sx = 1, + sy = 1, + r = 0, + offset = tileset.tileoffset, + } + + -- Be aware that in collections self.tiles can be a sparse array + self.tiles[tile.gid] = tile + end + + return gid + #tileset.tiles end --- Create Layers @@ -318,13 +307,19 @@ end function Map:setLayer(layer, path) if layer.encoding then if layer.encoding == "base64" then - assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".") + assert( + require("ffi"), + 'Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to "CSV".' + ) local fd = love.data.decode("string", "base64", layer.data) if not layer.compression then layer.data = utils.get_decompressed_data(fd) else - assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".") + assert( + love.data.decompress, + 'zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to "Base64 (uncompressed)" or "CSV".' + ) if layer.compression == "zlib" then local data = love.data.decompress("string", "zlib", fd) @@ -339,21 +334,27 @@ function Map:setLayer(layer, path) end end - layer.x = (layer.x or 0) + layer.offsetx + self.offsetx - layer.y = (layer.y or 0) + layer.offsety + self.offsety + layer.x = (layer.x or 0) + layer.offsetx + self.offsetx + layer.y = (layer.y or 0) + layer.offsety + self.offsety layer.update = function() end if layer.type == "tilelayer" then self:setTileData(layer) self:setSpriteBatches(layer) - layer.draw = function() self:drawTileLayer(layer) end + layer.draw = function() + self:drawTileLayer(layer) + end elseif layer.type == "objectgroup" then self:setObjectData(layer) self:setObjectCoordinates(layer) self:setObjectSpriteBatches(layer) - layer.draw = function() self:drawObjectLayer(layer) end + layer.draw = function() + self:drawObjectLayer(layer) + end elseif layer.type == "imagelayer" then - layer.draw = function() self:drawImageLayer(layer) end + layer.draw = function() + self:drawImageLayer(layer) + end if layer.image ~= "" then local formatted_path = utils.format_path(path .. layer.image) @@ -361,8 +362,8 @@ function Map:setLayer(layer, path) utils.cache_image(STI, formatted_path) end - layer.image = STI.cache[formatted_path] - layer.width = layer.image:getWidth() + layer.image = STI.cache[formatted_path] + layer.width = layer.image:getWidth() layer.height = layer.image:getHeight() end end @@ -380,7 +381,7 @@ function Map:setTileData(layer) return end - local i = 1 + local i = 1 local map = {} for y = 1, layer.height do @@ -404,7 +405,7 @@ end -- @param layer The Object Layer function Map:setObjectData(layer) for _, object in ipairs(layer.objects) do - object.layer = layer + object.layer = layer self.objects[object.id] = object end end @@ -413,10 +414,10 @@ end -- @param layer The Object Layer function Map:setObjectCoordinates(layer) for _, object in ipairs(layer.objects) do - local x = layer.x + object.x - local y = layer.y + object.y - local w = object.width - local h = object.height + local x = layer.x + object.x + local y = layer.y + object.y + local w = object.width + local h = object.height local cos = math.cos(math.rad(object.rotation)) local sin = math.sin(math.rad(object.rotation)) @@ -424,10 +425,10 @@ function Map:setObjectCoordinates(layer) object.rectangle = {} local vertices = { - { x=x, y=y }, - { x=x + w, y=y }, - { x=x + w, y=y + h }, - { x=x, y=y + h }, + { x = x, y = y }, + { x = x + w, y = y }, + { x = x + w, y = y + h }, + { x = x, y = y + h }, } for _, vertex in ipairs(vertices) do @@ -444,14 +445,14 @@ function Map:setObjectCoordinates(layer) end elseif object.shape == "polygon" then for _, vertex in ipairs(object.polygon) do - vertex.x = vertex.x + x - vertex.y = vertex.y + y + vertex.x = vertex.x + x + vertex.y = vertex.y + y vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) end elseif object.shape == "polyline" then for _, vertex in ipairs(object.polyline) do - vertex.x = vertex.x + x - vertex.y = vertex.y + y + vertex.x = vertex.x + x + vertex.y = vertex.y + y vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) end end @@ -527,16 +528,16 @@ end -- @param number Tile location on Y axis (in tiles) function Map:addNewLayerTile(layer, chunk, tile, x, y) local tileset = tile.tileset - local image = self.tilesets[tile.tileset].image + local image = self.tilesets[tile.tileset].image local batches local size if chunk then batches = chunk.batches - size = chunk.width * chunk.height + size = chunk.width * chunk.height else batches = layer.batches - size = layer.width * layer.height + size = layer.width * layer.height end batches[tileset] = batches[tileset] or lg.newSpriteBatch(image, size) @@ -547,11 +548,11 @@ function Map:addNewLayerTile(layer, chunk, tile, x, y) local instance = { layer = layer, chunk = chunk, - gid = tile.gid, - x = tileX, - y = tileY, - r = tile.r, - oy = 0 + gid = tile.gid, + x = tileX, + y = tileY, + r = tile.r, + oy = 0, } -- NOTE: STI can run headless so it is not guaranteed that a batch exists. @@ -575,10 +576,10 @@ function Map:set_batches(layer, chunk) local offsetX = chunk and chunk.x or 0 local offsetY = chunk and chunk.y or 0 - local startX = 1 - local startY = 1 - local endX = chunk and chunk.width or layer.width - local endY = chunk and chunk.height or layer.height + local startX = 1 + local startY = 1 + local endX = chunk and chunk.width or layer.width + local endY = chunk and chunk.height or layer.height local incrementX = 1 local incrementY = 1 @@ -683,7 +684,7 @@ end -- @param layer The Object Layer function Map:setObjectSpriteBatches(layer) local newBatch = lg.newSpriteBatch - local batches = {} + local batches = {} if layer.draworder == "topdown" then table.sort(layer.objects, function(a, b) @@ -693,13 +694,13 @@ function Map:setObjectSpriteBatches(layer) for _, object in ipairs(layer.objects) do if object.gid then - local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) + local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) local tileset = tile.tileset - local image = self.tilesets[tileset].image + local image = self.tilesets[tileset].image batches[tileset] = batches[tileset] or newBatch(image) - local sx = object.width / tile.width + local sx = object.width / tile.width local sy = object.height / tile.height -- Tiled rotates around bottom left corner, where love2D rotates around top left corner @@ -731,14 +732,14 @@ function Map:setObjectSpriteBatches(layer) end local instance = { - id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, ox, oy), + id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, ox, oy), batch = batch, layer = layer, - gid = tile.gid, - x = tileX, - y = tileY - oy, - r = tileR, - oy = oy + gid = tile.gid, + x = tileX, + y = tileY - oy, + r = tileR, + oy = oy, } self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} @@ -756,12 +757,12 @@ end function Map:addCustomLayer(name, index) index = index or #self.layers + 1 local layer = { - type = "customlayer", - name = name, - visible = true, - opacity = 1, - properties = {}, - } + type = "customlayer", + name = name, + visible = true, + opacity = 1, + properties = {}, + } function layer.draw() end function layer.update() end @@ -778,16 +779,16 @@ end function Map:convertToCustomLayer(index) local layer = assert(self.layers[index], "Layer not found: " .. index) - layer.type = "customlayer" - layer.x = nil - layer.y = nil - layer.width = nil - layer.height = nil + layer.type = "customlayer" + layer.x = nil + layer.y = nil + layer.width = nil + layer.height = nil layer.encoding = nil - layer.data = nil - layer.chunks = nil - layer.objects = nil - layer.image = nil + layer.data = nil + layer.chunks = nil + layer.objects = nil + layer.image = nil function layer.draw() end function layer.update() end @@ -862,16 +863,19 @@ function Map:update(dt) tile.time = tile.time + dt * 1000 while tile.time > tonumber(tile.animation[tile.frame].duration) do - update = true - tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) + update = true + tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) tile.frame = tile.frame + 1 - if tile.frame > #tile.animation then tile.frame = 1 end + if tile.frame > #tile.animation then + tile.frame = 1 + end end if update and self.tileInstances[tile.gid] then for _, j in pairs(self.tileInstances[tile.gid]) do - local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] + local t = + self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy) end end @@ -897,7 +901,7 @@ function Map:draw(tx, ty, sx, sy) -- Map is translated to correct position so the right section is drawn lg.push() lg.origin() - + --[[ This snippet comes from 'monolifed' on the Love2D forums, however it was more or less exactly the same code I was already writing @@ -948,18 +952,18 @@ end --- Draw an individual Layer -- @param layer The Layer to draw function Map.drawLayer(_, layer) - local r,g,b,a = lg.getColor() + local r, g, b, a = lg.getColor() -- if the layer has a tintcolor set - if layer.tintcolor then + if layer.tintcolor then r, g, b, a = unpack(layer.tintcolor) a = a or 255 -- alpha may not be specified - lg.setColor(r/255, g/255, b/255, a/255) -- Tiled uses 0-255 + lg.setColor(r / 255, g / 255, b / 255, a / 255) -- Tiled uses 0-255 -- if a tintcolor is not given just use the current color else lg.setColor(r, g, b, a * layer.opacity) end layer:draw() - lg.setColor(r,g,b,a) + lg.setColor(r, g, b, a) end --- Default draw function for Tile Layers @@ -996,10 +1000,10 @@ function Map:drawObjectLayer(layer) assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup") - local line = { 160, 160, 160, 255 * layer.opacity } - local fill = { 160, 160, 160, 255 * layer.opacity * 0.5 } - local r,g,b,a = lg.getColor() - local reset = { r, g, b, a * layer.opacity } + local line = { 160, 160, 160, 255 * layer.opacity } + local fill = { 160, 160, 160, 255 * layer.opacity * 0.5 } + local r, g, b, a = lg.getColor() + local reset = { r, g, b, a * layer.opacity } local function sortVertices(obj) local vertex = {} @@ -1058,7 +1062,7 @@ function Map:drawObjectLayer(layer) for _, batch in pairs(layer.batches) do lg.draw(batch, 0, 0) end - lg.setColor(r,g,b,a) + lg.setColor(r, g, b, a) end --- Default draw function for Image Layers @@ -1078,12 +1082,12 @@ function Map:drawImageLayer(layer) if layer.repeaty then local x = imagewidth local y = imageheight - while y < self.height * self.tileheight do + while y < self.height * self.tileheight do lg.draw(layer.image, x, y) -- if we are *also* repeating on X - if layer.repeatx then + if layer.repeatx then x = x + imagewidth - while x < self.width * self.tilewidth do + while x < self.width * self.tilewidth do lg.draw(layer.image, x, y) x = x + imagewidth end @@ -1093,7 +1097,7 @@ function Map:drawImageLayer(layer) -- if we're repeating on X alone... elseif layer.repeatx then local x = imagewidth - while x < self.width * self.tilewidth do + while x < self.width * self.tilewidth do lg.draw(layer.image, x, layer.y) x = x + imagewidth end @@ -1118,51 +1122,51 @@ end -- @param gid The flagged Global ID -- @return table Flipped Tile function Map:setFlippedGID(gid) - local bit31 = 2147483648 - local bit30 = 1073741824 - local bit29 = 536870912 - local flipX = false - local flipY = false - local flipD = false + local bit31 = 2147483648 + local bit30 = 1073741824 + local bit29 = 536870912 + local flipX = false + local flipY = false + local flipD = false local realgid = gid if realgid >= bit31 then realgid = realgid - bit31 - flipX = not flipX + flipX = not flipX end if realgid >= bit30 then realgid = realgid - bit30 - flipY = not flipY + flipY = not flipY end if realgid >= bit29 then realgid = realgid - bit29 - flipD = not flipD + flipD = not flipD end local tile = self.tiles[realgid] local data = { - id = tile.id, - gid = gid, - tileset = tile.tileset, - frame = tile.frame, - time = tile.time, - width = tile.width, - height = tile.height, - offset = tile.offset, - quad = tile.quad, + id = tile.id, + gid = gid, + tileset = tile.tileset, + frame = tile.frame, + time = tile.time, + width = tile.width, + height = tile.height, + offset = tile.offset, + quad = tile.quad, properties = tile.properties, - terrain = tile.terrain, - animation = tile.animation, - sx = tile.sx, - sy = tile.sy, - r = tile.r, + terrain = tile.terrain, + animation = tile.animation, + sx = tile.sx, + sy = tile.sy, + r = tile.r, } if flipX then if flipY and flipD then - data.r = math.rad(-90) + data.r = math.rad(-90) data.sy = -1 elseif flipY then data.sx = -1 @@ -1179,7 +1183,7 @@ function Map:setFlippedGID(gid) data.sy = -1 end elseif flipD then - data.r = math.rad(90) + data.r = math.rad(90) data.sy = -1 end @@ -1284,22 +1288,9 @@ function Map:swapTile(instance, tile) -- Update sprite batch if instance.batch then if tile then - instance.batch:set( - instance.id, - tile.quad, - instance.x, - instance.y, - tile.r, - tile.sx, - tile.sy - ) + instance.batch:set(instance.id, tile.quad, instance.x, instance.y, tile.r, tile.sx, tile.sy) else - instance.batch:set( - instance.id, - instance.x, - instance.y, - 0, - 0) + instance.batch:set(instance.id, instance.x, instance.y, 0, 0) self.freeBatchSprites[instance.batch] = self.freeBatchSprites[instance.batch] or {} table.insert(self.freeBatchSprites[instance.batch], instance) @@ -1329,12 +1320,12 @@ function Map:swapTile(instance, tile) newInstance.layer = instance.layer newInstance.batch = instance.batch - newInstance.id = instance.id - newInstance.gid = tile.gid or 0 - newInstance.x = instance.x - newInstance.y = instance.y - newInstance.r = tile.r or 0 - newInstance.oy = tile.r ~= 0 and tile.height or 0 + newInstance.id = instance.id + newInstance.gid = tile.gid or 0 + newInstance.x = instance.x + newInstance.y = instance.y + newInstance.r = tile.r or 0 + newInstance.oy = tile.r ~= 0 and tile.height or 0 table.insert(self.tileInstances[tile.gid], newInstance) end end @@ -1344,35 +1335,26 @@ end -- @param y The Y axis location of the point (in tiles) -- @return number The X axis location of the point (in pixels) -- @return number The Y axis location of the point (in pixels) -function Map:convertTileToPixel(x,y) +function Map:convertTileToPixel(x, y) if self.orientation == "orthogonal" then local tileW = self.tilewidth local tileH = self.tileheight - return - x * tileW, - y * tileH + return x * tileW, y * tileH elseif self.orientation == "isometric" then - local mapH = self.height - local tileW = self.tilewidth - local tileH = self.tileheight + local mapH = self.height + local tileW = self.tilewidth + local tileH = self.tileheight local offsetX = mapH * tileW / 2 - return - (x - y) * tileW / 2 + offsetX, - (x + y) * tileH / 2 - elseif self.orientation == "staggered" or - self.orientation == "hexagonal" then - local tileW = self.tilewidth - local tileH = self.tileheight + return (x - y) * tileW / 2 + offsetX, (x + y) * tileH / 2 + elseif self.orientation == "staggered" or self.orientation == "hexagonal" then + local tileW = self.tilewidth + local tileH = self.tileheight local sideLen = self.hexsidelength or 0 if self.staggeraxis == "x" then - return - x * tileW, - ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0) + return x * tileW, ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0) else - return - ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0), - y * tileH + return ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0), y * tileH end end end @@ -1386,20 +1368,16 @@ function Map:convertPixelToTile(x, y) if self.orientation == "orthogonal" then local tileW = self.tilewidth local tileH = self.tileheight - return - x / tileW, - y / tileH + return x / tileW, y / tileH elseif self.orientation == "isometric" then - local mapH = self.height - local tileW = self.tilewidth - local tileH = self.tileheight + local mapH = self.height + local tileW = self.tilewidth + local tileH = self.tileheight local offsetX = mapH * tileW / 2 - return - y / tileH + (x - offsetX) / tileW, - y / tileH - (x - offsetX) / tileW + return y / tileH + (x - offsetX) / tileW, y / tileH - (x - offsetX) / tileW elseif self.orientation == "staggered" then - local staggerX = self.staggeraxis == "x" - local even = self.staggerindex == "even" + local staggerX = self.staggeraxis == "x" + local even = self.staggerindex == "even" local function topLeft(x, y) if staggerX then @@ -1474,48 +1452,48 @@ function Map:convertPixelToTile(x, y) y = y - (even and tileH / 2 or 0) end - local halfH = tileH / 2 - local ratio = tileH / tileW + local halfH = tileH / 2 + local ratio = tileH / tileW local referenceX = ceil(x / tileW) local referenceY = ceil(y / tileH) - local relativeX = x - referenceX * tileW - local relativeY = y - referenceY * tileH + local relativeX = x - referenceX * tileW + local relativeY = y - referenceY * tileH - if (halfH - relativeX * ratio > relativeY) then + if halfH - relativeX * ratio > relativeY then return topLeft(referenceX, referenceY) - elseif (-halfH + relativeX * ratio > relativeY) then + elseif -halfH + relativeX * ratio > relativeY then return topRight(referenceX, referenceY) - elseif (halfH + relativeX * ratio < relativeY) then + elseif halfH + relativeX * ratio < relativeY then return bottomLeft(referenceX, referenceY) - elseif (halfH * 3 - relativeX * ratio < relativeY) then + elseif halfH * 3 - relativeX * ratio < relativeY then return bottomRight(referenceX, referenceY) end return referenceX, referenceY elseif self.orientation == "hexagonal" then - local staggerX = self.staggeraxis == "x" - local even = self.staggerindex == "even" - local tileW = self.tilewidth - local tileH = self.tileheight - local sideLenX = 0 - local sideLenY = 0 - - local colW = tileW / 2 - local rowH = tileH / 2 + local staggerX = self.staggeraxis == "x" + local even = self.staggerindex == "even" + local tileW = self.tilewidth + local tileH = self.tileheight + local sideLenX = 0 + local sideLenY = 0 + + local colW = tileW / 2 + local rowH = tileH / 2 if staggerX then sideLenX = self.hexsidelength x = x - (even and tileW or (tileW - sideLenX) / 2) - colW = colW - (colW - sideLenX / 2) / 2 + colW = colW - (colW - sideLenX / 2) / 2 else sideLenY = self.hexsidelength y = y - (even and tileH or (tileH - sideLenY) / 2) - rowH = rowH - (rowH - sideLenY / 2) / 2 + rowH = rowH - (rowH - sideLenY / 2) / 2 end local referenceX = ceil(x) / (colW * 2) local referenceY = ceil(y) / (rowH * 2) - -- If in staggered line, then shift reference by 0.5 of other axes + -- If in staggered line, then shift reference by 0.5 of other axes if staggerX then if (floor(referenceX) % 2 == 0) == even then referenceY = referenceY - 0.5 @@ -1526,31 +1504,31 @@ function Map:convertPixelToTile(x, y) end end - local relativeX = x - referenceX * colW * 2 - local relativeY = y - referenceY * rowH * 2 + local relativeX = x - referenceX * colW * 2 + local relativeY = y - referenceY * rowH * 2 local centers if staggerX then - local left = sideLenX / 2 + local left = sideLenX / 2 local centerX = left + colW local centerY = tileH / 2 centers = { - { x = left, y = centerY }, - { x = centerX, y = centerY - rowH }, - { x = centerX, y = centerY + rowH }, - { x = centerX + colW, y = centerY }, + { x = left, y = centerY }, + { x = centerX, y = centerY - rowH }, + { x = centerX, y = centerY + rowH }, + { x = centerX + colW, y = centerY }, } else - local top = sideLenY / 2 + local top = sideLenY / 2 local centerX = tileW / 2 local centerY = top + rowH centers = { - { x = centerX, y = top }, + { x = centerX, y = top }, { x = centerX - colW, y = centerY }, { x = centerX + colW, y = centerY }, - { x = centerX, y = centerY + rowH } + { x = centerX, y = centerY + rowH }, } end @@ -1571,24 +1549,22 @@ function Map:convertPixelToTile(x, y) end local offsetsStaggerX = { - { x = 1, y = 1 }, - { x = 2, y = 0 }, - { x = 2, y = 1 }, - { x = 3, y = 1 }, + { x = 1, y = 1 }, + { x = 2, y = 0 }, + { x = 2, y = 1 }, + { x = 3, y = 1 }, } local offsetsStaggerY = { - { x = 1, y = 1 }, - { x = 0, y = 2 }, - { x = 1, y = 2 }, - { x = 1, y = 3 }, + { x = 1, y = 1 }, + { x = 0, y = 2 }, + { x = 1, y = 2 }, + { x = 1, y = 3 }, } local offsets = staggerX and offsetsStaggerX or offsetsStaggerY - return - referenceX + offsets[nearest].x, - referenceY + offsets[nearest].y + return referenceX + offsets[nearest].x, referenceY + offsets[nearest].y end end diff --git a/libs/sti/plugins/box2d.lua b/libs/sti/plugins/box2d.lua index c6d4148..15acc16 100644 --- a/libs/sti/plugins/box2d.lua +++ b/libs/sti/plugins/box2d.lua @@ -4,14 +4,14 @@ -- @copyright 2019 -- @license MIT/X11 -local love = _G.love -local utils = require((...):gsub('plugins.box2d', 'utils')) -local lg = require((...):gsub('plugins.box2d', 'graphics')) +local love = _G.love +local utils = require((...):gsub("plugins.box2d", "utils")) +local lg = require((...):gsub("plugins.box2d", "graphics")) return { - box2d_LICENSE = "MIT/X11", - box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation", - box2d_VERSION = "2.3.2.7", + box2d_LICENSE = "MIT/X11", + box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation", + box2d_VERSION = "2.3.2.7", box2d_DESCRIPTION = "Box2D hooks for STI.", --- Initialize Box2D physics world. @@ -19,7 +19,7 @@ return { box2d_init = function(map, world) assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.") - local body = love.physics.newBody(world, map.offsetx, map.offsety) + local body = love.physics.newBody(world, map.offsetx, map.offsety) local collision = { body = body, } @@ -40,32 +40,32 @@ return { local currentBody = body --dynamic are objects/players etc. if userdata.properties.dynamic == true then - currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic') + currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "dynamic") -- static means it shouldn't move. Things like walls/ground. elseif userdata.properties.static == true then - currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'static') + currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "static") -- kinematic means that the object is static in the game world but effects other bodies elseif userdata.properties.kinematic == true then - currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'kinematic') + currentBody = love.physics.newBody(world, map.offsetx, map.offsety, "kinematic") end local fixture = love.physics.newFixture(currentBody, shape) fixture:setUserData(userdata) -- Set some custom properties from userdata (or use default set by box2d) - fixture:setFriction(userdata.properties.friction or 0.2) + fixture:setFriction(userdata.properties.friction or 0.2) fixture:setRestitution(userdata.properties.restitution or 0.0) - fixture:setSensor(userdata.properties.sensor or false) + fixture:setSensor(userdata.properties.sensor or false) fixture:setFilterData( userdata.properties.categories or 1, - userdata.properties.mask or 65535, - userdata.properties.group or 0 + userdata.properties.mask or 65535, + userdata.properties.group or 0 ) local obj = { - object = object, - body = currentBody, - shape = shape, + object = object, + body = currentBody, + shape = shape, fixture = fixture, } @@ -84,33 +84,33 @@ return { local function calculateObjectPosition(object, tile) local o = { - shape = object.shape, - x = (object.dx or object.x) + map.offsetx, - y = (object.dy or object.y) + map.offsety, - w = object.width, - h = object.height, - polygon = object.polygon or object.polyline or object.ellipse or object.rectangle + shape = object.shape, + x = (object.dx or object.x) + map.offsetx, + y = (object.dy or object.y) + map.offsety, + w = object.width, + h = object.height, + polygon = object.polygon or object.polyline or object.ellipse or object.rectangle, } local userdata = { - object = o, - properties = object.properties + object = o, + properties = object.properties, } o.r = object.rotation or 0 if o.shape == "rectangle" then local cos = math.cos(math.rad(o.r)) local sin = math.sin(math.rad(o.r)) - local oy = 0 + local oy = 0 if object.gid then local tileset = map.tilesets[map.tiles[object.gid].tileset] - local lid = object.gid - tileset.firstgid - local t = {} + local lid = object.gid - tileset.firstgid + local t = {} -- This fixes a height issue - o.y = o.y + map.tiles[object.gid].offset.y - oy = o.h + o.y = o.y + map.tiles[object.gid].offset.y + oy = o.h for _, tt in ipairs(tileset.tiles) do if tt.id == lid then @@ -133,10 +133,10 @@ return { end o.polygon = { - { x=o.x+0, y=o.y+0 }, - { x=o.x+o.w, y=o.y+0 }, - { x=o.x+o.w, y=o.y+o.h }, - { x=o.x+0, y=o.y+o.h } + { x = o.x + 0, y = o.y + 0 }, + { x = o.x + o.w, y = o.y + 0 }, + { x = o.x + o.w, y = o.y + o.h }, + { x = o.x + 0, y = o.y + o.h }, } for _, vertex in ipairs(o.polygon) do @@ -149,7 +149,7 @@ return { if not o.polygon then o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h) end - local vertices = getPolygonVertices(o) + local vertices = getPolygonVertices(o) local triangles = love.math.triangulate(vertices) for _, triangle in ipairs(triangles) do @@ -167,7 +167,7 @@ return { end end - local vertices = getPolygonVertices(o) + local vertices = getPolygonVertices(o) local triangles = love.math.triangulate(vertices) for _, triangle in ipairs(triangles) do @@ -197,12 +197,12 @@ return { -- Every instance of a tile if tile.properties.collidable == true then local object = { - shape = "rectangle", - x = instance.x, - y = instance.y, - width = map.tilewidth, - height = map.tileheight, - properties = tile.properties + shape = "rectangle", + x = instance.x, + y = instance.y, + width = map.tilewidth, + height = map.tileheight, + properties = tile.properties, } calculateObjectPosition(object, instance) @@ -222,12 +222,12 @@ return { for _, instance in ipairs(tiles) do if instance.layer == layer then local object = { - shape = "rectangle", - x = instance.x, - y = instance.y, - width = tileset.tilewidth, - height = tileset.tileheight, - properties = tile.properties + shape = "rectangle", + x = instance.x, + y = instance.y, + width = tileset.tilewidth, + height = tileset.tileheight, + properties = tile.properties, } calculateObjectPosition(object, instance) @@ -240,12 +240,12 @@ return { end elseif layer.type == "imagelayer" then local object = { - shape = "rectangle", - x = layer.x or 0, - y = layer.y or 0, - width = layer.width, - height = layer.height, - properties = layer.properties + shape = "rectangle", + x = layer.x or 0, + y = layer.y or 0, + width = layer.width, + height = layer.height, + properties = layer.properties, } calculateObjectPosition(object) @@ -295,7 +295,7 @@ return { lg.translate(math.floor(tx or 0), math.floor(ty or 0)) for _, obj in ipairs(collision) do - local points = {obj.body:getWorldPoints(obj.shape:getPoints())} + local points = { obj.body:getWorldPoints(obj.shape:getPoints()) } local shape_type = obj.shape:getType() if shape_type == "edge" or shape_type == "chain" then @@ -303,12 +303,12 @@ return { elseif shape_type == "polygon" then love.graphics.polygon("line", points) else - error("sti box2d plugin does not support "..shape_type.." shapes") + error("sti box2d plugin does not support " .. shape_type .. " shapes") end end lg.pop() - end + end, } --- Custom Properties in Tiled are used to tell this plugin what to do. diff --git a/libs/sti/plugins/bump.lua b/libs/sti/plugins/bump.lua index 1d4b828..b4a1328 100644 --- a/libs/sti/plugins/bump.lua +++ b/libs/sti/plugins/bump.lua @@ -4,13 +4,13 @@ -- @copyright 2019 -- @license MIT/X11 -local lg = require((...):gsub('plugins.bump', 'graphics')) +local lg = require((...):gsub("plugins.bump", "graphics")) return { - bump_LICENSE = "MIT/X11", - bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation", - bump_VERSION = "3.1.7.1", - bump_DESCRIPTION = "Bump hooks for STI.", + bump_LICENSE = "MIT/X11", + bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation", + bump_VERSION = "3.1.7.1", + bump_DESCRIPTION = "Bump hooks for STI.", --- Adds each collidable tile to the Bump world. -- @param world The Bump world to add objects to. @@ -29,15 +29,14 @@ return { for _, object in ipairs(tile.objectGroup.objects) do if object.properties.collidable == true then local t = { - name = object.name, - type = object.type, - x = instance.x + map.offsetx + object.x, - y = instance.y + map.offsety + object.y, - width = object.width, - height = object.height, - layer = instance.layer, - properties = object.properties - + name = object.name, + type = object.type, + x = instance.x + map.offsetx + object.x, + y = instance.y + map.offsety + object.y, + width = object.width, + height = object.height, + layer = instance.layer, + properties = object.properties, } world:add(t, t.x, t.y, t.width, t.height) @@ -49,13 +48,13 @@ return { -- Every instance of a tile if tile.properties and tile.properties.collidable == true then local t = { - x = instance.x + map.offsetx, - y = instance.y + map.offsety, - width = map.tilewidth, - height = map.tileheight, - layer = instance.layer, - type = tile.type, - properties = tile.properties + x = instance.x + map.offsetx, + y = instance.y + map.offsety, + width = map.tilewidth, + height = map.tileheight, + layer = instance.layer, + type = tile.type, + properties = tile.properties, } world:add(t, t.x, t.y, t.width, t.height) @@ -72,19 +71,18 @@ return { if layer.type == "tilelayer" then for y, tiles in ipairs(layer.data) do for x, tile in pairs(tiles) do - if tile.objectGroup then for _, object in ipairs(tile.objectGroup.objects) do if object.properties.collidable == true then local t = { - name = object.name, - type = object.type, - x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x, - y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y, - width = object.width, - height = object.height, - layer = layer, - properties = object.properties + name = object.name, + type = object.type, + x = ((x - 1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x, + y = ((y - 1) * map.tileheight + tile.offset.y + map.offsety) + object.y, + width = object.width, + height = object.height, + layer = layer, + properties = object.properties, } world:add(t, t.x, t.y, t.width, t.height) @@ -93,15 +91,14 @@ return { end end - local t = { - x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx, - y = (y-1) * map.tileheight + tile.offset.y + map.offsety, - width = tile.width, - height = tile.height, - layer = layer, - type = tile.type, - properties = tile.properties + x = (x - 1) * map.tilewidth + tile.offset.x + map.offsetx, + y = (y - 1) * map.tileheight + tile.offset.y + map.offsety, + width = tile.width, + height = tile.height, + layer = layer, + type = tile.type, + properties = tile.properties, } world:add(t, t.x, t.y, t.width, t.height) @@ -112,23 +109,23 @@ return { world:add(layer, layer.x, layer.y, layer.width, layer.height) table.insert(collidables, layer) end - end + end -- individual collidable objects in a layer that is not "collidable" -- or whole collidable objects layer - if layer.type == "objectgroup" then + if layer.type == "objectgroup" then for _, obj in ipairs(layer.objects) do if layer.properties.collidable == true or obj.properties.collidable == true then if obj.shape == "rectangle" then local t = { - name = obj.name, - type = obj.type, - x = obj.x + map.offsetx, - y = obj.y + map.offsety, - width = obj.width, - height = obj.height, - layer = layer, - properties = obj.properties + name = obj.name, + type = obj.type, + x = obj.x + map.offsetx, + y = obj.y + map.offsety, + width = obj.width, + height = obj.height, + layer = layer, + properties = obj.properties, } if obj.gid then @@ -143,7 +140,7 @@ return { end end - map.bump_world = world + map.bump_world = world map.bump_collidables = collidables end, @@ -157,11 +154,7 @@ return { for i = #collidables, 1, -1 do local obj = collidables[i] - if obj.layer == layer - and ( - layer.properties.collidable == true - or obj.properties.collidable == true - ) then + if obj.layer == layer and (layer.properties.collidable == true or obj.properties.collidable == true) then map.bump_world:remove(obj) table.remove(collidables, i) end @@ -185,7 +178,7 @@ return { end lg.pop() - end + end, } --- Custom Properties in Tiled are used to tell this plugin what to do. diff --git a/libs/sti/utils.lua b/libs/sti/utils.lua index 95e857a..e42132a 100644 --- a/libs/sti/utils.lua +++ b/libs/sti/utils.lua @@ -3,19 +3,21 @@ local utils = {} -- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286 function utils.format_path(path) - local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP' - local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/') + local np_gen1, np_gen2 = "[^SEP]+SEP%.%.SEP?", "SEP+%.?SEP" + local np_pat1, np_pat2 = np_gen1:gsub("SEP", "/"), np_gen2:gsub("SEP", "/") local k repeat -- /./ -> / - path,k = path:gsub(np_pat2,'/',1) + path, k = path:gsub(np_pat2, "/", 1) until k == 0 repeat -- A/../ -> (empty) - path,k = path:gsub(np_pat1,'',1) + path, k = path:gsub(np_pat1, "", 1) until k == 0 - if path == '' then path = '.' end + if path == "" then + path = "." + end return path end @@ -25,8 +27,12 @@ function utils.compensate(tile, tileX, tileY, tileW, tileH) local compx = 0 local compy = 0 - if tile.sx < 0 then compx = tileW end - if tile.sy < 0 then compy = tileH end + if tile.sx < 0 then + compx = tileW + end + if tile.sy < 0 then + compy = tileH + end if tile.r > 0 then tileX = tileX + tileH - compy @@ -51,13 +57,17 @@ end -- We just don't know. function utils.get_tiles(imageW, tileW, margin, spacing) - imageW = imageW - margin + imageW = imageW - margin local n = 0 while imageW >= tileW do imageW = imageW - tileW - if n ~= 0 then imageW = imageW - spacing end - if imageW >= 0 then n = n + 1 end + if n ~= 0 then + imageW = imageW - spacing + end + if imageW >= 0 then + n = n + 1 + end end return n @@ -65,8 +75,8 @@ end -- Decompress tile layer data function utils.get_decompressed_data(data) - local ffi = require "ffi" - local d = {} + local ffi = require("ffi") + local d = {} local decoded = ffi.cast("uint32_t*", data) for i = 0, data:len() / ffi.sizeof("uint32_t") do @@ -79,8 +89,8 @@ end -- Convert a Tiled ellipse object to a LOVE polygon function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) local ceil = math.ceil - local cos = math.cos - local sin = math.sin + local cos = math.cos + local sin = math.sin local function calc_segments(segments) local function vdist(a, b) @@ -95,7 +105,7 @@ function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) segments = segments or 64 local vertices = {} - local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) } + local v = { 1, 2, ceil(segments / 4 - 1), ceil(segments / 4) } local m if love and love.physics then @@ -106,8 +116,8 @@ function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) for _, i in ipairs(v) do local angle = (i / segments) * math.pi * 2 - local px = x + w / 2 + cos(angle) * w / 2 - local py = y + h / 2 + sin(angle) * h / 2 + local px = x + w / 2 + cos(angle) * w / 2 + local py = y + h / 2 + sin(angle) * h / 2 table.insert(vertices, { x = px / m, y = py / m }) end @@ -117,7 +127,7 @@ function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) -- Box2D threshold if dist1 < 0.0025 or dist2 < 0.0025 then - return calc_segments(segments-2) + return calc_segments(segments - 2) end return segments @@ -130,8 +140,8 @@ function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) for i = 0, segments do local angle = (i / segments) * math.pi * 2 - local px = x + w / 2 + cos(angle) * w / 2 - local py = y + h / 2 + sin(angle) * h / 2 + local px = x + w / 2 + cos(angle) * w / 2 + local py = y + h / 2 + sin(angle) * h / 2 table.insert(vertices, { x = px, y = py }) end @@ -141,30 +151,26 @@ end function utils.rotate_vertex(map, vertex, x, y, cos, sin, oy) if map.orientation == "isometric" then - x, y = utils.convert_isometric_to_screen(map, x, y) + x, y = utils.convert_isometric_to_screen(map, x, y) vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y) end vertex.x = vertex.x - x vertex.y = vertex.y - y - return - x + cos * vertex.x - sin * vertex.y, - y + sin * vertex.x + cos * vertex.y - (oy or 0) + return x + cos * vertex.x - sin * vertex.y, y + sin * vertex.x + cos * vertex.y - (oy or 0) end --- Project isometric position to cartesian position function utils.convert_isometric_to_screen(map, x, y) - local mapW = map.width - local tileW = map.tilewidth - local tileH = map.tileheight - local tileX = x / tileH - local tileY = y / tileH + local mapW = map.width + local tileW = map.tilewidth + local tileH = map.tileheight + local tileX = x / tileH + local tileY = y / tileH local offsetX = mapW * tileW / 2 - return - (tileX - tileY) * tileW / 2 + offsetX, - (tileX + tileY) * tileH / 2 + return (tileX - tileY) * tileW / 2 + offsetX, (tileX + tileY) * tileH / 2 end function utils.hex_to_color(hex) @@ -175,16 +181,14 @@ function utils.hex_to_color(hex) return { r = tonumber(hex:sub(1, 2), 16) / 255, g = tonumber(hex:sub(3, 4), 16) / 255, - b = tonumber(hex:sub(5, 6), 16) / 255 + b = tonumber(hex:sub(5, 6), 16) / 255, } end function utils.pixel_function(_, _, r, g, b, a) local mask = utils._TC - if r == mask.r and - g == mask.g and - b == mask.b then + if r == mask.r and g == mask.g and b == mask.b then return r, g, b, 0 end @@ -205,7 +209,7 @@ end function utils.deepCopy(t) local copy = {} - for k,v in pairs(t) do + for k, v in pairs(t) do if type(v) == "table" then v = utils.deepCopy(v) end diff --git a/libs/windfield/init.lua b/libs/windfield/init.lua index 8554822..b0b36cf 100644 --- a/libs/windfield/init.lua +++ b/libs/windfield/init.lua @@ -1,929 +1,1136 @@ ---[[ -The MIT License (MIT) - -Copyright (c) 2018 SSYGEN - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]]-- - -local path = ... .. '.' -local wf = {} -wf.Math = require(path .. 'mlib.mlib') - -World = {} -World.__index = World - -function wf.newWorld(xg, yg, sleep) - local world = wf.World.new(wf, xg, yg, sleep) - - world.box2d_world:setCallbacks(world.collisionOnEnter, world.collisionOnExit, world.collisionPre, world.collisionPost) - world:collisionClear() - world:addCollisionClass('Default') - - -- Points all box2d_world functions to this wf.World object - -- This means that the user can call world:setGravity for instance without having to say world.box2d_world:setGravity - for k, v in pairs(world.box2d_world.__index) do - if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'update' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then - world[k] = function(self, ...) - return v(self.box2d_world, ...) - end - end - end - - return world -end - -function World.new(wf, xg, yg, sleep) - local self = {} - local settings = settings or {} - self.wf = wf - - self.draw_query_for_n_frames = 10 - self.query_debug_drawing_enabled = false - self.explicit_collision_events = false - self.collision_classes = {} - self.masks = {} - self.is_sensor_memo = {} - self.query_debug_draw = {} - - love.physics.setMeter(32) - self.box2d_world = love.physics.newWorld(xg, yg, sleep) - - return setmetatable(self, World) -end - -function World:update(dt) - self:collisionEventsClear() - self.box2d_world:update(dt) -end - -function World:draw(alpha) - -- get the current color values to reapply - local r, g, b, a = love.graphics.getColor() - -- alpha value is optional - alpha = alpha or 255 - -- Colliders debug - love.graphics.setColor(222, 222, 222, alpha) - local bodies = self.box2d_world:getBodies() - for _, body in ipairs(bodies) do - local fixtures = body:getFixtures() - for _, fixture in ipairs(fixtures) do - if fixture:getShape():type() == 'PolygonShape' then - love.graphics.polygon('line', body:getWorldPoints(fixture:getShape():getPoints())) - elseif fixture:getShape():type() == 'EdgeShape' or fixture:getShape():type() == 'ChainShape' then - local points = {body:getWorldPoints(fixture:getShape():getPoints())} - for i = 1, #points, 2 do - if i < #points-2 then love.graphics.line(points[i], points[i+1], points[i+2], points[i+3]) end - end - elseif fixture:getShape():type() == 'CircleShape' then - local body_x, body_y = body:getPosition() - local shape_x, shape_y = fixture:getShape():getPoint() - local r = fixture:getShape():getRadius() - love.graphics.circle('line', body_x + shape_x, body_y + shape_y, r, 360) - end - end - end - love.graphics.setColor(255, 255, 255, alpha) - - -- Joint debug - love.graphics.setColor(222, 128, 64, alpha) - local joints = self.box2d_world:getJoints() - for _, joint in ipairs(joints) do - local x1, y1, x2, y2 = joint:getAnchors() - if x1 and y1 then love.graphics.circle('line', x1, y1, 4) end - if x2 and y2 then love.graphics.circle('line', x2, y2, 4) end - end - love.graphics.setColor(255, 255, 255, alpha) - - -- Query debug - love.graphics.setColor(64, 64, 222, alpha) - for _, query_draw in ipairs(self.query_debug_draw) do - query_draw.frames = query_draw.frames - 1 - if query_draw.type == 'circle' then - love.graphics.circle('line', query_draw.x, query_draw.y, query_draw.r) - elseif query_draw.type == 'rectangle' then - love.graphics.rectangle('line', query_draw.x, query_draw.y, query_draw.w, query_draw.h) - elseif query_draw.type == 'line' then - love.graphics.line(query_draw.x1, query_draw.y1, query_draw.x2, query_draw.y2) - elseif query_draw.type == 'polygon' then - local triangles = love.math.triangulate(query_draw.vertices) - for _, triangle in ipairs(triangles) do love.graphics.polygon('line', triangle) end - end - end - for i = #self.query_debug_draw, 1, -1 do - if self.query_debug_draw[i].frames <= 0 then - table.remove(self.query_debug_draw, i) - end - end - love.graphics.setColor(r, g, b, a) -end - -function World:setQueryDebugDrawing(value) - self.query_debug_drawing_enabled = value -end - -function World:setExplicitCollisionEvents(value) - self.explicit_collision_events = value -end - -function World:addCollisionClass(collision_class_name, collision_class) - if self.collision_classes[collision_class_name] then error('Collision class ' .. collision_class_name .. ' already exists.') end - - if self.explicit_collision_events then - self.collision_classes[collision_class_name] = collision_class or {} - else - self.collision_classes[collision_class_name] = collision_class or {} - self.collision_classes[collision_class_name].enter = {} - self.collision_classes[collision_class_name].exit = {} - self.collision_classes[collision_class_name].pre = {} - self.collision_classes[collision_class_name].post = {} - for c_class_name, _ in pairs(self.collision_classes) do - table.insert(self.collision_classes[collision_class_name].enter, c_class_name) - table.insert(self.collision_classes[collision_class_name].exit, c_class_name) - table.insert(self.collision_classes[collision_class_name].pre, c_class_name) - table.insert(self.collision_classes[collision_class_name].post, c_class_name) - end - for c_class_name, _ in pairs(self.collision_classes) do - table.insert(self.collision_classes[c_class_name].enter, collision_class_name) - table.insert(self.collision_classes[c_class_name].exit, collision_class_name) - table.insert(self.collision_classes[c_class_name].pre, collision_class_name) - table.insert(self.collision_classes[c_class_name].post, collision_class_name) - end - end - - self:collisionClassesSet() -end - -function World:collisionClassesSet() - self:generateCategoriesMasks() - - self:collisionClear() - local collision_table = self:getCollisionCallbacksTable() - for collision_class_name, collision_list in pairs(collision_table) do - for _, collision_info in ipairs(collision_list) do - if collision_info.type == 'enter' then self:addCollisionEnter(collision_class_name, collision_info.other) end - if collision_info.type == 'exit' then self:addCollisionExit(collision_class_name, collision_info.other) end - if collision_info.type == 'pre' then self:addCollisionPre(collision_class_name, collision_info.other) end - if collision_info.type == 'post' then self:addCollisionPost(collision_class_name, collision_info.other) end - end - end - - self:collisionEventsClear() -end - -function World:collisionClear() - self.collisions = {} - self.collisions.on_enter = {} - self.collisions.on_enter.sensor = {} - self.collisions.on_enter.non_sensor = {} - self.collisions.on_exit = {} - self.collisions.on_exit.sensor = {} - self.collisions.on_exit.non_sensor = {} - self.collisions.pre = {} - self.collisions.pre.sensor = {} - self.collisions.pre.non_sensor = {} - self.collisions.post = {} - self.collisions.post.sensor = {} - self.collisions.post.non_sensor = {} -end - -function World:collisionEventsClear() - local bodies = self.box2d_world:getBodies() - for _, body in ipairs(bodies) do - local collider = body:getFixtures()[1]:getUserData() - collider:collisionEventsClear() - end -end - -function World:addCollisionEnter(type1, type2) - if not self:isCollisionBetweenSensors(type1, type2) then - table.insert(self.collisions.on_enter.non_sensor, {type1 = type1, type2 = type2}) - else table.insert(self.collisions.on_enter.sensor, {type1 = type1, type2 = type2}) end -end - -function World:addCollisionExit(type1, type2) - if not self:isCollisionBetweenSensors(type1, type2) then - table.insert(self.collisions.on_exit.non_sensor, {type1 = type1, type2 = type2}) - else table.insert(self.collisions.on_exit.sensor, {type1 = type1, type2 = type2}) end -end - -function World:addCollisionPre(type1, type2) - if not self:isCollisionBetweenSensors(type1, type2) then - table.insert(self.collisions.pre.non_sensor, {type1 = type1, type2 = type2}) - else table.insert(self.collisions.pre.sensor, {type1 = type1, type2 = type2}) end -end - -function World:addCollisionPost(type1, type2) - if not self:isCollisionBetweenSensors(type1, type2) then - table.insert(self.collisions.post.non_sensor, {type1 = type1, type2 = type2}) - else table.insert(self.collisions.post.sensor, {type1 = type1, type2 = type2}) end -end - -function World:doesType1IgnoreType2(type1, type2) - local collision_ignores = {} - for collision_class_name, collision_class in pairs(self.collision_classes) do - collision_ignores[collision_class_name] = collision_class.ignores or {} - end - local all = {} - for collision_class_name, _ in pairs(collision_ignores) do - table.insert(all, collision_class_name) - end - local ignored_types = {} - for _, collision_class_type in ipairs(collision_ignores[type1]) do - if collision_class_type == 'All' then - for _, collision_class_name in ipairs(all) do - table.insert(ignored_types, collision_class_name) - end - else table.insert(ignored_types, collision_class_type) end - end - for key, _ in pairs(collision_ignores[type1]) do - if key == 'except' then - for _, except_type in ipairs(collision_ignores[type1].except) do - for i = #ignored_types, 1, -1 do - if ignored_types[i] == except_type then table.remove(ignored_types, i) end - end - end - end - end - for _, ignored_type in ipairs(ignored_types) do - if ignored_type == type2 then return true end - end -end - -function World:isCollisionBetweenSensors(type1, type2) - if not self.is_sensor_memo[type1] then self.is_sensor_memo[type1] = {} end - if not self.is_sensor_memo[type1][type2] then self.is_sensor_memo[type1][type2] = (self:doesType1IgnoreType2(type1, type2) or self:doesType1IgnoreType2(type2, type1)) end - if self.is_sensor_memo[type1][type2] then return true - else return false end -end - --- https://love2d.org/forums/viewtopic.php?f=4&t=75441 -function World:generateCategoriesMasks() - local collision_ignores = {} - for collision_class_name, collision_class in pairs(self.collision_classes) do - collision_ignores[collision_class_name] = collision_class.ignores or {} - end - local incoming = {} - local expanded = {} - local all = {} - for object_type, _ in pairs(collision_ignores) do - incoming[object_type] = {} - expanded[object_type] = {} - table.insert(all, object_type) - end - for object_type, ignore_list in pairs(collision_ignores) do - for key, ignored_type in pairs(ignore_list) do - if ignored_type == 'All' then - for _, all_object_type in ipairs(all) do - table.insert(incoming[all_object_type], object_type) - table.insert(expanded[object_type], all_object_type) - end - elseif type(ignored_type) == 'string' then - if ignored_type ~= 'All' then - table.insert(incoming[ignored_type], object_type) - table.insert(expanded[object_type], ignored_type) - end - end - if key == 'except' then - for _, except_ignored_type in ipairs(ignored_type) do - for i, v in ipairs(incoming[except_ignored_type]) do - if v == object_type then - table.remove(incoming[except_ignored_type], i) - break - end - end - end - for _, except_ignored_type in ipairs(ignored_type) do - for i, v in ipairs(expanded[object_type]) do - if v == except_ignored_type then - table.remove(expanded[object_type], i) - break - end - end - end - end - end - end - local edge_groups = {} - for k, v in pairs(incoming) do - table.sort(v, function(a, b) return string.lower(a) < string.lower(b) end) - end - local i = 0 - for k, v in pairs(incoming) do - local str = "" - for _, c in ipairs(v) do - str = str .. c - end - if not edge_groups[str] then i = i + 1; edge_groups[str] = {n = i} end - table.insert(edge_groups[str], k) - end - local categories = {} - for k, _ in pairs(collision_ignores) do - categories[k] = {} - end - for k, v in pairs(edge_groups) do - for i, c in ipairs(v) do - categories[c] = v.n - end - end - for k, v in pairs(expanded) do - local category = {categories[k]} - local current_masks = {} - for _, c in ipairs(v) do - table.insert(current_masks, categories[c]) - end - self.masks[k] = {categories = category, masks = current_masks} - end -end - -function World:getCollisionCallbacksTable() - local collision_table = {} - for collision_class_name, collision_class in pairs(self.collision_classes) do - collision_table[collision_class_name] = {} - for _, v in ipairs(collision_class.enter or {}) do table.insert(collision_table[collision_class_name], {type = 'enter', other = v}) end - for _, v in ipairs(collision_class.exit or {}) do table.insert(collision_table[collision_class_name], {type = 'exit', other = v}) end - for _, v in ipairs(collision_class.pre or {}) do table.insert(collision_table[collision_class_name], {type = 'pre', other = v}) end - for _, v in ipairs(collision_class.post or {}) do table.insert(collision_table[collision_class_name], {type = 'post', other = v}) end - end - return collision_table -end - -local function collEnsure(collision_class_name1, a, collision_class_name2, b) - if a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1 then return b, a - else return a, b end -end - -local function collIf(collision_class_name1, collision_class_name2, a, b) - if (a.collision_class == collision_class_name1 and b.collision_class == collision_class_name2) or - (a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1) then - return true - else return false end -end - -function World.collisionOnEnter(fixture_a, fixture_b, contact) - local a, b = fixture_a:getUserData(), fixture_b:getUserData() - - if fixture_a:isSensor() and fixture_b:isSensor() then - if a and b then - for _, collision in ipairs(a.world.collisions.on_enter.sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - table.insert(a.collision_events[collision.type2], {collision_type = 'enter', collider_1 = a, collider_2 = b, contact = contact}) - if collision.type1 == collision.type2 then - table.insert(b.collision_events[collision.type1], {collision_type = 'enter', collider_1 = b, collider_2 = a, contact = contact}) - end - end - end - end - - elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then - if a and b then - for _, collision in ipairs(a.world.collisions.on_enter.non_sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - table.insert(a.collision_events[collision.type2], {collision_type = 'enter', collider_1 = a, collider_2 = b, contact = contact}) - if collision.type1 == collision.type2 then - table.insert(b.collision_events[collision.type1], {collision_type = 'enter', collider_1 = b, collider_2 = a, contact = contact}) - end - end - end - end - end -end - -function World.collisionOnExit(fixture_a, fixture_b, contact) - local a, b = fixture_a:getUserData(), fixture_b:getUserData() - - if fixture_a:isSensor() and fixture_b:isSensor() then - if a and b then - for _, collision in ipairs(a.world.collisions.on_exit.sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - table.insert(a.collision_events[collision.type2], {collision_type = 'exit', collider_1 = a, collider_2 = b, contact = contact}) - if collision.type1 == collision.type2 then - table.insert(b.collision_events[collision.type1], {collision_type = 'exit', collider_1 = b, collider_2 = a, contact = contact}) - end - end - end - end - - elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then - if a and b then - for _, collision in ipairs(a.world.collisions.on_exit.non_sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - table.insert(a.collision_events[collision.type2], {collision_type = 'exit', collider_1 = a, collider_2 = b, contact = contact}) - if collision.type1 == collision.type2 then - table.insert(b.collision_events[collision.type1], {collision_type = 'exit', collider_1 = b, collider_2 = a, contact = contact}) - end - end - end - end - end -end - -function World.collisionPre(fixture_a, fixture_b, contact) - local a, b = fixture_a:getUserData(), fixture_b:getUserData() - - if fixture_a:isSensor() and fixture_b:isSensor() then - if a and b then - for _, collision in ipairs(a.world.collisions.pre.sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - a:preSolve(b, contact) - if collision.type1 == collision.type2 then - b:preSolve(a, contact) - end - end - end - end - - elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then - if a and b then - for _, collision in ipairs(a.world.collisions.pre.non_sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - a:preSolve(b, contact) - if collision.type1 == collision.type2 then - b:preSolve(a, contact) - end - end - end - end - end -end - -function World.collisionPost(fixture_a, fixture_b, contact, ni1, ti1, ni2, ti2) - local a, b = fixture_a:getUserData(), fixture_b:getUserData() - - if fixture_a:isSensor() and fixture_b:isSensor() then - if a and b then - for _, collision in ipairs(a.world.collisions.post.sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - a:postSolve(b, contact, ni1, ti1, ni2, ti2) - if collision.type1 == collision.type2 then - b:postSolve(a, contact, ni1, ti1, ni2, ti2) - end - end - end - end - - elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then - if a and b then - for _, collision in ipairs(a.world.collisions.post.non_sensor) do - if collIf(collision.type1, collision.type2, a, b) then - a, b = collEnsure(collision.type1, a, collision.type2, b) - a:postSolve(b, contact, ni1, ti1, ni2, ti2) - if collision.type1 == collision.type2 then - b:postSolve(a, contact, ni1, ti1, ni2, ti2) - end - end - end - end - end -end - -function World:newCircleCollider(x, y, r, settings) - return self.wf.Collider.new(self, 'Circle', x, y, r, settings) -end - -function World:newRectangleCollider(x, y, w, h, settings) - return self.wf.Collider.new(self, 'Rectangle', x, y, w, h, settings) -end - -function World:newBSGRectangleCollider(x, y, w, h, corner_cut_size, settings) - return self.wf.Collider.new(self, 'BSGRectangle', x, y, w, h, corner_cut_size, settings) -end - -function World:newPolygonCollider(vertices, settings) - return self.wf.Collider.new(self, 'Polygon', vertices, settings) -end - -function World:newLineCollider(x1, y1, x2, y2, settings) - return self.wf.Collider.new(self, 'Line', x1, y1, x2, y2, settings) -end - -function World:newChainCollider(vertices, loop, settings) - return self.wf.Collider.new(self, 'Chain', vertices, loop, settings) -end - --- Internal AABB box2d query used before going for more specific and precise computations. -function World:_queryBoundingBox(x1, y1, x2, y2) - local colliders = {} - local callback = function(fixture) - if not fixture:isSensor() then table.insert(colliders, fixture:getUserData()) end - return true - end - self.box2d_world:queryBoundingBox(x1, y1, x2, y2, callback) - return colliders -end - -function World:collisionClassInCollisionClassesList(collision_class, collision_classes) - if collision_classes[1] == 'All' then - local all_collision_classes = {} - for class, _ in pairs(self.collision_classes) do - table.insert(all_collision_classes, class) - end - if collision_classes.except then - for _, except in ipairs(collision_classes.except) do - for i, class in ipairs(all_collision_classes) do - if class == except then - table.remove(all_collision_classes, i) - break - end - end - end - end - for _, class in ipairs(all_collision_classes) do - if class == collision_class then return true end - end - else - for _, class in ipairs(collision_classes) do - if class == collision_class then return true end - end - end -end - -function World:queryCircleArea(x, y, radius, collision_class_names) - if not collision_class_names then collision_class_names = {'All'} end - if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'circle', x = x, y = y, r = radius, frames = self.draw_query_for_n_frames}) end - - local colliders = self:_queryBoundingBox(x-radius, y-radius, x+radius, y+radius) - local outs = {} - for _, collider in ipairs(colliders) do - if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then - for _, fixture in ipairs(collider.body:getFixtures()) do - if self.wf.Math.polygon.getCircleIntersection(x, y, radius, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then - table.insert(outs, collider) - break - end - end - end - end - return outs -end - -function World:queryRectangleArea(x, y, w, h, collision_class_names) - if not collision_class_names then collision_class_names = {'All'} end - if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'rectangle', x = x, y = y, w = w, h = h, frames = self.draw_query_for_n_frames}) end - - local colliders = self:_queryBoundingBox(x, y, x+w, y+h) - local outs = {} - for _, collider in ipairs(colliders) do - if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then - for _, fixture in ipairs(collider.body:getFixtures()) do - if self.wf.Math.polygon.isPolygonInside({x, y, x+w, y, x+w, y+h, x, y+h}, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then - table.insert(outs, collider) - break - end - end - end - end - return outs -end - -function World:queryPolygonArea(vertices, collision_class_names) - if not collision_class_names then collision_class_names = {'All'} end - if self.query_debug_drawing_enabled then table.insert(self.query_debug_draw, {type = 'polygon', vertices = vertices, frames = self.draw_query_for_n_frames}) end - - local cx, cy = self.wf.Math.polygon.getCentroid(vertices) - local d_max = 0 - for i = 1, #vertices, 2 do - local d = self.wf.Math.line.getLength(cx, cy, vertices[i], vertices[i+1]) - if d > d_max then d_max = d end - end - local colliders = self:_queryBoundingBox(cx-d_max, cy-d_max, cx+d_max, cy+d_max) - local outs = {} - for _, collider in ipairs(colliders) do - if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then - for _, fixture in ipairs(collider.body:getFixtures()) do - if self.wf.Math.polygon.isPolygonInside(vertices, {collider.body:getWorldPoints(fixture:getShape():getPoints())}) then - table.insert(outs, collider) - break - end - end - end - end - return outs -end - -function World:queryLine(x1, y1, x2, y2, collision_class_names) - if not collision_class_names then collision_class_names = {'All'} end - if self.query_debug_drawing_enabled then - table.insert(self.query_debug_draw, {type = 'line', x1 = x1, y1 = y1, x2 = x2, y2 = y2, frames = self.draw_query_for_n_frames}) - end - - local colliders = {} - local callback = function(fixture, ...) - if not fixture:isSensor() then table.insert(colliders, fixture:getUserData()) end - return 1 - end - self.box2d_world:rayCast(x1, y1, x2, y2, callback) - - local outs = {} - for _, collider in ipairs(colliders) do - if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then - table.insert(outs, collider) - end - end - return outs -end - -function World:addJoint(joint_type, ...) - local args = {...} - if args[1].body then args[1] = args[1].body end - if type(args[2]) == "table" and args[2].body then args[2] = args[2].body end - local joint = love.physics['new' .. joint_type](unpack(args)) - return joint -end - -function World:removeJoint(joint) - joint:destroy() -end - -function World:destroy() - local bodies = self.box2d_world:getBodies() - for _, body in ipairs(bodies) do - local collider = body:getFixtures()[1]:getUserData() - collider:destroy() - end - local joints = self.box2d_world:getJoints() - for _, joint in ipairs(joints) do joint:destroy() end - self.box2d_world:destroy() - self.box2d_world = nil -end - - - -local Collider = {} -Collider.__index = Collider - -local generator = love.math.newRandomGenerator(os.time()) -local function UUID() - local fn = function(x) - local r = generator:random(16) - 1 - r = (x == "x") and (r + 1) or (r % 4) + 9 - return ("0123456789abcdef"):sub(r, r) - end - return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) -end - -function Collider.new(world, collider_type, ...) - local self = {} - self.id = UUID() - self.world = world - self.type = collider_type - self.object = nil - - self.shapes = {} - self.fixtures = {} - self.sensors = {} - - self.collision_events = {} - self.collision_stay = {} - self.enter_collision_data = {} - self.exit_collision_data = {} - self.stay_collision_data = {} - - local args = {...} - local shape, fixture - if self.type == 'Circle' then - self.collision_class = (args[4] and args[4].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, args[1], args[2], (args[4] and args[4].body_type) or 'dynamic') - shape = love.physics.newCircleShape(args[3]) - - elseif self.type == 'Rectangle' then - self.collision_class = (args[5] and args[5].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, args[1] + args[3]/2, args[2] + args[4]/2, (args[5] and args[5].body_type) or 'dynamic') - shape = love.physics.newRectangleShape(args[3], args[4]) - - elseif self.type == 'BSGRectangle' then - self.collision_class = (args[6] and args[6].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, args[1] + args[3]/2, args[2] + args[4]/2, (args[6] and args[6].body_type) or 'dynamic') - local w, h, s = args[3], args[4], args[5] - shape = love.physics.newPolygonShape({ - -w/2, -h/2 + s, -w/2 + s, -h/2, - w/2 - s, -h/2, w/2, -h/2 + s, - w/2, h/2 - s, w/2 - s, h/2, - -w/2 + s, h/2, -w/2, h/2 - s - }) - - elseif self.type == 'Polygon' then - self.collision_class = (args[2] and args[2].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[2] and args[2].body_type) or 'dynamic') - shape = love.physics.newPolygonShape(unpack(args[1])) - - elseif self.type == 'Line' then - self.collision_class = (args[5] and args[5].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[5] and args[5].body_type) or 'dynamic') - shape = love.physics.newEdgeShape(args[1], args[2], args[3], args[4]) - - elseif self.type == 'Chain' then - self.collision_class = (args[3] and args[3].collision_class) or 'Default' - self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[3] and args[3].body_type) or 'dynamic') - shape = love.physics.newChainShape(args[1], unpack(args[2])) - end - - -- Define collision classes and attach them to fixture and sensor - fixture = love.physics.newFixture(self.body, shape) - if self.world.masks[self.collision_class] then - fixture:setCategory(unpack(self.world.masks[self.collision_class].categories)) - fixture:setMask(unpack(self.world.masks[self.collision_class].masks)) - end - fixture:setUserData(self) - local sensor = love.physics.newFixture(self.body, shape) - sensor:setSensor(true) - sensor:setUserData(self) - - self.shapes['main'] = shape - self.fixtures['main'] = fixture - self.sensors['main'] = sensor - self.shape = shape - self.fixture = fixture - - self.preSolve = function() end - self.postSolve = function() end - - -- Points all body, fixture and shape functions to this wf.Collider object - -- This means that the user can call collider:setLinearVelocity for instance without having to say collider.body:setLinearVelocity - for k, v in pairs(self.body.__index) do - if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then - self[k] = function(self, ...) - return v(self.body, ...) - end - end - end - for k, v in pairs(self.fixture.__index) do - if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then - self[k] = function(self, ...) - return v(self.fixture, ...) - end - end - end - for k, v in pairs(self.shape.__index) do - if k ~= '__gc' and k ~= '__eq' and k ~= '__index' and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' and k ~= 'typeOf' then - self[k] = function(self, ...) - return v(self.shape, ...) - end - end - end - - return setmetatable(self, Collider) -end - -function Collider:collisionEventsClear() - self.collision_events = {} - for other, _ in pairs(self.world.collision_classes) do - self.collision_events[other] = {} - end -end - -function Collider:setCollisionClass(collision_class_name) - if not self.world.collision_classes[collision_class_name] then error("Collision class " .. collision_class_name .. " doesn't exist.") end - self.collision_class = collision_class_name - for _, fixture in pairs(self.fixtures) do - if self.world.masks[collision_class_name] then - fixture:setCategory(unpack(self.world.masks[collision_class_name].categories)) - fixture:setMask(unpack(self.world.masks[collision_class_name].masks)) - end - end -end - -function Collider:enter(other_collision_class_name) - local events = self.collision_events[other_collision_class_name] - if events and #events >= 1 then - for _, e in ipairs(events) do - if e.collision_type == 'enter' then - if not self.collision_stay[other_collision_class_name] then self.collision_stay[other_collision_class_name] = {} end - table.insert(self.collision_stay[other_collision_class_name], {collider = e.collider_2, contact = e.contact}) - self.enter_collision_data[other_collision_class_name] = {collider = e.collider_2, contact = e.contact} - return true - end - end - end -end - -function Collider:getEnterCollisionData(other_collision_class_name) - return self.enter_collision_data[other_collision_class_name] -end - -function Collider:exit(other_collision_class_name) - local events = self.collision_events[other_collision_class_name] - if events and #events >= 1 then - for _, e in ipairs(events) do - if e.collision_type == 'exit' then - if self.collision_stay[other_collision_class_name] then - for i = #self.collision_stay[other_collision_class_name], 1, -1 do - local collision_stay = self.collision_stay[other_collision_class_name][i] - if collision_stay.collider.id == e.collider_2.id then table.remove(self.collision_stay[other_collision_class_name], i) end - end - end - self.exit_collision_data[other_collision_class_name] = {collider = e.collider_2, contact = e.contact} - return true - end - end - end -end - -function Collider:getExitCollisionData(other_collision_class_name) - return self.exit_collision_data[other_collision_class_name] -end - -function Collider:stay(other_collision_class_name) - if self.collision_stay[other_collision_class_name] then - if #self.collision_stay[other_collision_class_name] >= 1 then - return true - end - end -end - -function Collider:getStayCollisionData(other_collision_class_name) - return self.collision_stay[other_collision_class_name] -end - -function Collider:setPreSolve(callback) - self.preSolve = callback -end - -function Collider:setPostSolve(callback) - self.postSolve = callback -end - -function Collider:setObject(object) - self.object = object -end - -function Collider:getObject() - return self.object -end - -function Collider:addShape(shape_name, shape_type, ...) - if self.shapes[shape_name] or self.fixtures[shape_name] then error("Shape/fixture " .. shape_name .. " already exists.") end - local args = {...} - local shape = love.physics['new' .. shape_type](unpack(args)) - local fixture = love.physics.newFixture(self.body, shape) - if self.world.masks[self.collision_class] then - fixture:setCategory(unpack(self.world.masks[self.collision_class].categories)) - fixture:setMask(unpack(self.world.masks[self.collision_class].masks)) - end - fixture:setUserData(self) - local sensor = love.physics.newFixture(self.body, shape) - sensor:setSensor(true) - sensor:setUserData(self) - - self.shapes[shape_name] = shape - self.fixtures[shape_name] = fixture - self.sensors[shape_name] = sensor -end - -function Collider:removeShape(shape_name) - if not self.shapes[shape_name] then return end - self.shapes[shape_name] = nil - self.fixtures[shape_name]:setUserData(nil) - self.fixtures[shape_name]:destroy() - self.fixtures[shape_name] = nil - self.sensors[shape_name]:setUserData(nil) - self.sensors[shape_name]:destroy() - self.sensors[shape_name] = nil -end - -function Collider:destroy() - self.collision_stay = nil - self.enter_collision_data = nil - self.exit_collision_data = nil - self:collisionEventsClear() - - self:setObject(nil) - for name, _ in pairs(self.fixtures) do - self.shapes[name] = nil - self.fixtures[name]:setUserData(nil) - self.fixtures[name] = nil - self.sensors[name]:setUserData(nil) - self.sensors[name] = nil - end - self.body:destroy() - self.body = nil -end - -wf.World = World -wf.Collider = Collider - -return wf - +--[[ +The MIT License (MIT) + +Copyright (c) 2018 SSYGEN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] +-- + +local path = ... .. "." +local wf = {} +wf.Math = require(path .. "mlib.mlib") + +World = {} +World.__index = World + +function wf.newWorld(xg, yg, sleep) + local world = wf.World.new(wf, xg, yg, sleep) + + world.box2d_world:setCallbacks( + world.collisionOnEnter, + world.collisionOnExit, + world.collisionPre, + world.collisionPost + ) + world:collisionClear() + world:addCollisionClass("Default") + + -- Points all box2d_world functions to this wf.World object + -- This means that the user can call world:setGravity for instance without having to say world.box2d_world:setGravity + for k, v in pairs(world.box2d_world.__index) do + if + k ~= "__gc" + and k ~= "__eq" + and k ~= "__index" + and k ~= "__tostring" + and k ~= "update" + and k ~= "destroy" + and k ~= "type" + and k ~= "typeOf" + then + world[k] = function(self, ...) + return v(self.box2d_world, ...) + end + end + end + + return world +end + +function World.new(wf, xg, yg, sleep) + local self = {} + local settings = settings or {} + self.wf = wf + + self.draw_query_for_n_frames = 10 + self.query_debug_drawing_enabled = false + self.explicit_collision_events = false + self.collision_classes = {} + self.masks = {} + self.is_sensor_memo = {} + self.query_debug_draw = {} + + love.physics.setMeter(32) + self.box2d_world = love.physics.newWorld(xg, yg, sleep) + + return setmetatable(self, World) +end + +function World:update(dt) + self:collisionEventsClear() + self.box2d_world:update(dt) +end + +function World:draw(alpha) + -- get the current color values to reapply + local r, g, b, a = love.graphics.getColor() + -- alpha value is optional + alpha = alpha or 255 + -- Colliders debug + love.graphics.setColor(222, 222, 222, alpha) + local bodies = self.box2d_world:getBodies() + for _, body in ipairs(bodies) do + local fixtures = body:getFixtures() + for _, fixture in ipairs(fixtures) do + if fixture:getShape():type() == "PolygonShape" then + love.graphics.polygon("line", body:getWorldPoints(fixture:getShape():getPoints())) + elseif fixture:getShape():type() == "EdgeShape" or fixture:getShape():type() == "ChainShape" then + local points = { body:getWorldPoints(fixture:getShape():getPoints()) } + for i = 1, #points, 2 do + if i < #points - 2 then + love.graphics.line(points[i], points[i + 1], points[i + 2], points[i + 3]) + end + end + elseif fixture:getShape():type() == "CircleShape" then + local body_x, body_y = body:getPosition() + local shape_x, shape_y = fixture:getShape():getPoint() + local r = fixture:getShape():getRadius() + love.graphics.circle("line", body_x + shape_x, body_y + shape_y, r, 360) + end + end + end + love.graphics.setColor(255, 255, 255, alpha) + + -- Joint debug + love.graphics.setColor(222, 128, 64, alpha) + local joints = self.box2d_world:getJoints() + for _, joint in ipairs(joints) do + local x1, y1, x2, y2 = joint:getAnchors() + if x1 and y1 then + love.graphics.circle("line", x1, y1, 4) + end + if x2 and y2 then + love.graphics.circle("line", x2, y2, 4) + end + end + love.graphics.setColor(255, 255, 255, alpha) + + -- Query debug + love.graphics.setColor(64, 64, 222, alpha) + for _, query_draw in ipairs(self.query_debug_draw) do + query_draw.frames = query_draw.frames - 1 + if query_draw.type == "circle" then + love.graphics.circle("line", query_draw.x, query_draw.y, query_draw.r) + elseif query_draw.type == "rectangle" then + love.graphics.rectangle("line", query_draw.x, query_draw.y, query_draw.w, query_draw.h) + elseif query_draw.type == "line" then + love.graphics.line(query_draw.x1, query_draw.y1, query_draw.x2, query_draw.y2) + elseif query_draw.type == "polygon" then + local triangles = love.math.triangulate(query_draw.vertices) + for _, triangle in ipairs(triangles) do + love.graphics.polygon("line", triangle) + end + end + end + for i = #self.query_debug_draw, 1, -1 do + if self.query_debug_draw[i].frames <= 0 then + table.remove(self.query_debug_draw, i) + end + end + love.graphics.setColor(r, g, b, a) +end + +function World:setQueryDebugDrawing(value) + self.query_debug_drawing_enabled = value +end + +function World:setExplicitCollisionEvents(value) + self.explicit_collision_events = value +end + +function World:addCollisionClass(collision_class_name, collision_class) + if self.collision_classes[collision_class_name] then + error("Collision class " .. collision_class_name .. " already exists.") + end + + if self.explicit_collision_events then + self.collision_classes[collision_class_name] = collision_class or {} + else + self.collision_classes[collision_class_name] = collision_class or {} + self.collision_classes[collision_class_name].enter = {} + self.collision_classes[collision_class_name].exit = {} + self.collision_classes[collision_class_name].pre = {} + self.collision_classes[collision_class_name].post = {} + for c_class_name, _ in pairs(self.collision_classes) do + table.insert(self.collision_classes[collision_class_name].enter, c_class_name) + table.insert(self.collision_classes[collision_class_name].exit, c_class_name) + table.insert(self.collision_classes[collision_class_name].pre, c_class_name) + table.insert(self.collision_classes[collision_class_name].post, c_class_name) + end + for c_class_name, _ in pairs(self.collision_classes) do + table.insert(self.collision_classes[c_class_name].enter, collision_class_name) + table.insert(self.collision_classes[c_class_name].exit, collision_class_name) + table.insert(self.collision_classes[c_class_name].pre, collision_class_name) + table.insert(self.collision_classes[c_class_name].post, collision_class_name) + end + end + + self:collisionClassesSet() +end + +function World:collisionClassesSet() + self:generateCategoriesMasks() + + self:collisionClear() + local collision_table = self:getCollisionCallbacksTable() + for collision_class_name, collision_list in pairs(collision_table) do + for _, collision_info in ipairs(collision_list) do + if collision_info.type == "enter" then + self:addCollisionEnter(collision_class_name, collision_info.other) + end + if collision_info.type == "exit" then + self:addCollisionExit(collision_class_name, collision_info.other) + end + if collision_info.type == "pre" then + self:addCollisionPre(collision_class_name, collision_info.other) + end + if collision_info.type == "post" then + self:addCollisionPost(collision_class_name, collision_info.other) + end + end + end + + self:collisionEventsClear() +end + +function World:collisionClear() + self.collisions = {} + self.collisions.on_enter = {} + self.collisions.on_enter.sensor = {} + self.collisions.on_enter.non_sensor = {} + self.collisions.on_exit = {} + self.collisions.on_exit.sensor = {} + self.collisions.on_exit.non_sensor = {} + self.collisions.pre = {} + self.collisions.pre.sensor = {} + self.collisions.pre.non_sensor = {} + self.collisions.post = {} + self.collisions.post.sensor = {} + self.collisions.post.non_sensor = {} +end + +function World:collisionEventsClear() + local bodies = self.box2d_world:getBodies() + for _, body in ipairs(bodies) do + local collider = body:getFixtures()[1]:getUserData() + collider:collisionEventsClear() + end +end + +function World:addCollisionEnter(type1, type2) + if not self:isCollisionBetweenSensors(type1, type2) then + table.insert(self.collisions.on_enter.non_sensor, { type1 = type1, type2 = type2 }) + else + table.insert(self.collisions.on_enter.sensor, { type1 = type1, type2 = type2 }) + end +end + +function World:addCollisionExit(type1, type2) + if not self:isCollisionBetweenSensors(type1, type2) then + table.insert(self.collisions.on_exit.non_sensor, { type1 = type1, type2 = type2 }) + else + table.insert(self.collisions.on_exit.sensor, { type1 = type1, type2 = type2 }) + end +end + +function World:addCollisionPre(type1, type2) + if not self:isCollisionBetweenSensors(type1, type2) then + table.insert(self.collisions.pre.non_sensor, { type1 = type1, type2 = type2 }) + else + table.insert(self.collisions.pre.sensor, { type1 = type1, type2 = type2 }) + end +end + +function World:addCollisionPost(type1, type2) + if not self:isCollisionBetweenSensors(type1, type2) then + table.insert(self.collisions.post.non_sensor, { type1 = type1, type2 = type2 }) + else + table.insert(self.collisions.post.sensor, { type1 = type1, type2 = type2 }) + end +end + +function World:doesType1IgnoreType2(type1, type2) + local collision_ignores = {} + for collision_class_name, collision_class in pairs(self.collision_classes) do + collision_ignores[collision_class_name] = collision_class.ignores or {} + end + local all = {} + for collision_class_name, _ in pairs(collision_ignores) do + table.insert(all, collision_class_name) + end + local ignored_types = {} + for _, collision_class_type in ipairs(collision_ignores[type1]) do + if collision_class_type == "All" then + for _, collision_class_name in ipairs(all) do + table.insert(ignored_types, collision_class_name) + end + else + table.insert(ignored_types, collision_class_type) + end + end + for key, _ in pairs(collision_ignores[type1]) do + if key == "except" then + for _, except_type in ipairs(collision_ignores[type1].except) do + for i = #ignored_types, 1, -1 do + if ignored_types[i] == except_type then + table.remove(ignored_types, i) + end + end + end + end + end + for _, ignored_type in ipairs(ignored_types) do + if ignored_type == type2 then + return true + end + end +end + +function World:isCollisionBetweenSensors(type1, type2) + if not self.is_sensor_memo[type1] then + self.is_sensor_memo[type1] = {} + end + if not self.is_sensor_memo[type1][type2] then + self.is_sensor_memo[type1][type2] = ( + self:doesType1IgnoreType2(type1, type2) or self:doesType1IgnoreType2(type2, type1) + ) + end + if self.is_sensor_memo[type1][type2] then + return true + else + return false + end +end + +-- https://love2d.org/forums/viewtopic.php?f=4&t=75441 +function World:generateCategoriesMasks() + local collision_ignores = {} + for collision_class_name, collision_class in pairs(self.collision_classes) do + collision_ignores[collision_class_name] = collision_class.ignores or {} + end + local incoming = {} + local expanded = {} + local all = {} + for object_type, _ in pairs(collision_ignores) do + incoming[object_type] = {} + expanded[object_type] = {} + table.insert(all, object_type) + end + for object_type, ignore_list in pairs(collision_ignores) do + for key, ignored_type in pairs(ignore_list) do + if ignored_type == "All" then + for _, all_object_type in ipairs(all) do + table.insert(incoming[all_object_type], object_type) + table.insert(expanded[object_type], all_object_type) + end + elseif type(ignored_type) == "string" then + if ignored_type ~= "All" then + table.insert(incoming[ignored_type], object_type) + table.insert(expanded[object_type], ignored_type) + end + end + if key == "except" then + for _, except_ignored_type in ipairs(ignored_type) do + for i, v in ipairs(incoming[except_ignored_type]) do + if v == object_type then + table.remove(incoming[except_ignored_type], i) + break + end + end + end + for _, except_ignored_type in ipairs(ignored_type) do + for i, v in ipairs(expanded[object_type]) do + if v == except_ignored_type then + table.remove(expanded[object_type], i) + break + end + end + end + end + end + end + local edge_groups = {} + for k, v in pairs(incoming) do + table.sort(v, function(a, b) + return string.lower(a) < string.lower(b) + end) + end + local i = 0 + for k, v in pairs(incoming) do + local str = "" + for _, c in ipairs(v) do + str = str .. c + end + if not edge_groups[str] then + i = i + 1 + edge_groups[str] = { n = i } + end + table.insert(edge_groups[str], k) + end + local categories = {} + for k, _ in pairs(collision_ignores) do + categories[k] = {} + end + for k, v in pairs(edge_groups) do + for i, c in ipairs(v) do + categories[c] = v.n + end + end + for k, v in pairs(expanded) do + local category = { categories[k] } + local current_masks = {} + for _, c in ipairs(v) do + table.insert(current_masks, categories[c]) + end + self.masks[k] = { categories = category, masks = current_masks } + end +end + +function World:getCollisionCallbacksTable() + local collision_table = {} + for collision_class_name, collision_class in pairs(self.collision_classes) do + collision_table[collision_class_name] = {} + for _, v in ipairs(collision_class.enter or {}) do + table.insert(collision_table[collision_class_name], { type = "enter", other = v }) + end + for _, v in ipairs(collision_class.exit or {}) do + table.insert(collision_table[collision_class_name], { type = "exit", other = v }) + end + for _, v in ipairs(collision_class.pre or {}) do + table.insert(collision_table[collision_class_name], { type = "pre", other = v }) + end + for _, v in ipairs(collision_class.post or {}) do + table.insert(collision_table[collision_class_name], { type = "post", other = v }) + end + end + return collision_table +end + +local function collEnsure(collision_class_name1, a, collision_class_name2, b) + if a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1 then + return b, a + else + return a, b + end +end + +local function collIf(collision_class_name1, collision_class_name2, a, b) + if + (a.collision_class == collision_class_name1 and b.collision_class == collision_class_name2) + or (a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1) + then + return true + else + return false + end +end + +function World.collisionOnEnter(fixture_a, fixture_b, contact) + local a, b = fixture_a:getUserData(), fixture_b:getUserData() + + if fixture_a:isSensor() and fixture_b:isSensor() then + if a and b then + for _, collision in ipairs(a.world.collisions.on_enter.sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + table.insert( + a.collision_events[collision.type2], + { collision_type = "enter", collider_1 = a, collider_2 = b, contact = contact } + ) + if collision.type1 == collision.type2 then + table.insert( + b.collision_events[collision.type1], + { collision_type = "enter", collider_1 = b, collider_2 = a, contact = contact } + ) + end + end + end + end + elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then + if a and b then + for _, collision in ipairs(a.world.collisions.on_enter.non_sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + table.insert( + a.collision_events[collision.type2], + { collision_type = "enter", collider_1 = a, collider_2 = b, contact = contact } + ) + if collision.type1 == collision.type2 then + table.insert( + b.collision_events[collision.type1], + { collision_type = "enter", collider_1 = b, collider_2 = a, contact = contact } + ) + end + end + end + end + end +end + +function World.collisionOnExit(fixture_a, fixture_b, contact) + local a, b = fixture_a:getUserData(), fixture_b:getUserData() + + if fixture_a:isSensor() and fixture_b:isSensor() then + if a and b then + for _, collision in ipairs(a.world.collisions.on_exit.sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + table.insert( + a.collision_events[collision.type2], + { collision_type = "exit", collider_1 = a, collider_2 = b, contact = contact } + ) + if collision.type1 == collision.type2 then + table.insert( + b.collision_events[collision.type1], + { collision_type = "exit", collider_1 = b, collider_2 = a, contact = contact } + ) + end + end + end + end + elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then + if a and b then + for _, collision in ipairs(a.world.collisions.on_exit.non_sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + table.insert( + a.collision_events[collision.type2], + { collision_type = "exit", collider_1 = a, collider_2 = b, contact = contact } + ) + if collision.type1 == collision.type2 then + table.insert( + b.collision_events[collision.type1], + { collision_type = "exit", collider_1 = b, collider_2 = a, contact = contact } + ) + end + end + end + end + end +end + +function World.collisionPre(fixture_a, fixture_b, contact) + local a, b = fixture_a:getUserData(), fixture_b:getUserData() + + if fixture_a:isSensor() and fixture_b:isSensor() then + if a and b then + for _, collision in ipairs(a.world.collisions.pre.sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + a:preSolve(b, contact) + if collision.type1 == collision.type2 then + b:preSolve(a, contact) + end + end + end + end + elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then + if a and b then + for _, collision in ipairs(a.world.collisions.pre.non_sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + a:preSolve(b, contact) + if collision.type1 == collision.type2 then + b:preSolve(a, contact) + end + end + end + end + end +end + +function World.collisionPost(fixture_a, fixture_b, contact, ni1, ti1, ni2, ti2) + local a, b = fixture_a:getUserData(), fixture_b:getUserData() + + if fixture_a:isSensor() and fixture_b:isSensor() then + if a and b then + for _, collision in ipairs(a.world.collisions.post.sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + a:postSolve(b, contact, ni1, ti1, ni2, ti2) + if collision.type1 == collision.type2 then + b:postSolve(a, contact, ni1, ti1, ni2, ti2) + end + end + end + end + elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then + if a and b then + for _, collision in ipairs(a.world.collisions.post.non_sensor) do + if collIf(collision.type1, collision.type2, a, b) then + a, b = collEnsure(collision.type1, a, collision.type2, b) + a:postSolve(b, contact, ni1, ti1, ni2, ti2) + if collision.type1 == collision.type2 then + b:postSolve(a, contact, ni1, ti1, ni2, ti2) + end + end + end + end + end +end + +function World:newCircleCollider(x, y, r, settings) + return self.wf.Collider.new(self, "Circle", x, y, r, settings) +end + +function World:newRectangleCollider(x, y, w, h, settings) + return self.wf.Collider.new(self, "Rectangle", x, y, w, h, settings) +end + +function World:newBSGRectangleCollider(x, y, w, h, corner_cut_size, settings) + return self.wf.Collider.new(self, "BSGRectangle", x, y, w, h, corner_cut_size, settings) +end + +function World:newPolygonCollider(vertices, settings) + return self.wf.Collider.new(self, "Polygon", vertices, settings) +end + +function World:newLineCollider(x1, y1, x2, y2, settings) + return self.wf.Collider.new(self, "Line", x1, y1, x2, y2, settings) +end + +function World:newChainCollider(vertices, loop, settings) + return self.wf.Collider.new(self, "Chain", vertices, loop, settings) +end + +-- Internal AABB box2d query used before going for more specific and precise computations. +function World:_queryBoundingBox(x1, y1, x2, y2) + local colliders = {} + local callback = function(fixture) + if not fixture:isSensor() then + table.insert(colliders, fixture:getUserData()) + end + return true + end + self.box2d_world:queryBoundingBox(x1, y1, x2, y2, callback) + return colliders +end + +function World:collisionClassInCollisionClassesList(collision_class, collision_classes) + if collision_classes[1] == "All" then + local all_collision_classes = {} + for class, _ in pairs(self.collision_classes) do + table.insert(all_collision_classes, class) + end + if collision_classes.except then + for _, except in ipairs(collision_classes.except) do + for i, class in ipairs(all_collision_classes) do + if class == except then + table.remove(all_collision_classes, i) + break + end + end + end + end + for _, class in ipairs(all_collision_classes) do + if class == collision_class then + return true + end + end + else + for _, class in ipairs(collision_classes) do + if class == collision_class then + return true + end + end + end +end + +function World:queryCircleArea(x, y, radius, collision_class_names) + if not collision_class_names then + collision_class_names = { "All" } + end + if self.query_debug_drawing_enabled then + table.insert( + self.query_debug_draw, + { type = "circle", x = x, y = y, r = radius, frames = self.draw_query_for_n_frames } + ) + end + + local colliders = self:_queryBoundingBox(x - radius, y - radius, x + radius, y + radius) + local outs = {} + for _, collider in ipairs(colliders) do + if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then + for _, fixture in ipairs(collider.body:getFixtures()) do + if + self.wf.Math.polygon.getCircleIntersection( + x, + y, + radius, + { collider.body:getWorldPoints(fixture:getShape():getPoints()) } + ) + then + table.insert(outs, collider) + break + end + end + end + end + return outs +end + +function World:queryRectangleArea(x, y, w, h, collision_class_names) + if not collision_class_names then + collision_class_names = { "All" } + end + if self.query_debug_drawing_enabled then + table.insert( + self.query_debug_draw, + { type = "rectangle", x = x, y = y, w = w, h = h, frames = self.draw_query_for_n_frames } + ) + end + + local colliders = self:_queryBoundingBox(x, y, x + w, y + h) + local outs = {} + for _, collider in ipairs(colliders) do + if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then + for _, fixture in ipairs(collider.body:getFixtures()) do + if + self.wf.Math.polygon.isPolygonInside( + { x, y, x + w, y, x + w, y + h, x, y + h }, + { collider.body:getWorldPoints(fixture:getShape():getPoints()) } + ) + then + table.insert(outs, collider) + break + end + end + end + end + return outs +end + +function World:queryPolygonArea(vertices, collision_class_names) + if not collision_class_names then + collision_class_names = { "All" } + end + if self.query_debug_drawing_enabled then + table.insert( + self.query_debug_draw, + { type = "polygon", vertices = vertices, frames = self.draw_query_for_n_frames } + ) + end + + local cx, cy = self.wf.Math.polygon.getCentroid(vertices) + local d_max = 0 + for i = 1, #vertices, 2 do + local d = self.wf.Math.line.getLength(cx, cy, vertices[i], vertices[i + 1]) + if d > d_max then + d_max = d + end + end + local colliders = self:_queryBoundingBox(cx - d_max, cy - d_max, cx + d_max, cy + d_max) + local outs = {} + for _, collider in ipairs(colliders) do + if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then + for _, fixture in ipairs(collider.body:getFixtures()) do + if + self.wf.Math.polygon.isPolygonInside( + vertices, + { collider.body:getWorldPoints(fixture:getShape():getPoints()) } + ) + then + table.insert(outs, collider) + break + end + end + end + end + return outs +end + +function World:queryLine(x1, y1, x2, y2, collision_class_names) + if not collision_class_names then + collision_class_names = { "All" } + end + if self.query_debug_drawing_enabled then + table.insert( + self.query_debug_draw, + { type = "line", x1 = x1, y1 = y1, x2 = x2, y2 = y2, frames = self.draw_query_for_n_frames } + ) + end + + local colliders = {} + local callback = function(fixture, ...) + if not fixture:isSensor() then + table.insert(colliders, fixture:getUserData()) + end + return 1 + end + self.box2d_world:rayCast(x1, y1, x2, y2, callback) + + local outs = {} + for _, collider in ipairs(colliders) do + if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then + table.insert(outs, collider) + end + end + return outs +end + +function World:addJoint(joint_type, ...) + local args = { ... } + if args[1].body then + args[1] = args[1].body + end + if type(args[2]) == "table" and args[2].body then + args[2] = args[2].body + end + local joint = love.physics["new" .. joint_type](unpack(args)) + return joint +end + +function World:removeJoint(joint) + joint:destroy() +end + +function World:destroy() + local bodies = self.box2d_world:getBodies() + for _, body in ipairs(bodies) do + local collider = body:getFixtures()[1]:getUserData() + collider:destroy() + end + local joints = self.box2d_world:getJoints() + for _, joint in ipairs(joints) do + joint:destroy() + end + self.box2d_world:destroy() + self.box2d_world = nil +end + +local Collider = {} +Collider.__index = Collider + +local generator = love.math.newRandomGenerator(os.time()) +local function UUID() + local fn = function(x) + local r = generator:random(16) - 1 + r = (x == "x") and (r + 1) or (r % 4) + 9 + return ("0123456789abcdef"):sub(r, r) + end + return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn)) +end + +function Collider.new(world, collider_type, ...) + local self = {} + self.id = UUID() + self.world = world + self.type = collider_type + self.object = nil + + self.shapes = {} + self.fixtures = {} + self.sensors = {} + + self.collision_events = {} + self.collision_stay = {} + self.enter_collision_data = {} + self.exit_collision_data = {} + self.stay_collision_data = {} + + local args = { ... } + local shape, fixture + if self.type == "Circle" then + self.collision_class = (args[4] and args[4].collision_class) or "Default" + self.body = + love.physics.newBody(self.world.box2d_world, args[1], args[2], (args[4] and args[4].body_type) or "dynamic") + shape = love.physics.newCircleShape(args[3]) + elseif self.type == "Rectangle" then + self.collision_class = (args[5] and args[5].collision_class) or "Default" + self.body = love.physics.newBody( + self.world.box2d_world, + args[1] + args[3] / 2, + args[2] + args[4] / 2, + (args[5] and args[5].body_type) or "dynamic" + ) + shape = love.physics.newRectangleShape(args[3], args[4]) + elseif self.type == "BSGRectangle" then + self.collision_class = (args[6] and args[6].collision_class) or "Default" + self.body = love.physics.newBody( + self.world.box2d_world, + args[1] + args[3] / 2, + args[2] + args[4] / 2, + (args[6] and args[6].body_type) or "dynamic" + ) + local w, h, s = args[3], args[4], args[5] + shape = love.physics.newPolygonShape({ + -w / 2, + -h / 2 + s, + -w / 2 + s, + -h / 2, + w / 2 - s, + -h / 2, + w / 2, + -h / 2 + s, + w / 2, + h / 2 - s, + w / 2 - s, + h / 2, + -w / 2 + s, + h / 2, + -w / 2, + h / 2 - s, + }) + elseif self.type == "Polygon" then + self.collision_class = (args[2] and args[2].collision_class) or "Default" + self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[2] and args[2].body_type) or "dynamic") + shape = love.physics.newPolygonShape(unpack(args[1])) + elseif self.type == "Line" then + self.collision_class = (args[5] and args[5].collision_class) or "Default" + self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[5] and args[5].body_type) or "dynamic") + shape = love.physics.newEdgeShape(args[1], args[2], args[3], args[4]) + elseif self.type == "Chain" then + self.collision_class = (args[3] and args[3].collision_class) or "Default" + self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[3] and args[3].body_type) or "dynamic") + shape = love.physics.newChainShape(args[1], unpack(args[2])) + end + + -- Define collision classes and attach them to fixture and sensor + fixture = love.physics.newFixture(self.body, shape) + if self.world.masks[self.collision_class] then + fixture:setCategory(unpack(self.world.masks[self.collision_class].categories)) + fixture:setMask(unpack(self.world.masks[self.collision_class].masks)) + end + fixture:setUserData(self) + local sensor = love.physics.newFixture(self.body, shape) + sensor:setSensor(true) + sensor:setUserData(self) + + self.shapes["main"] = shape + self.fixtures["main"] = fixture + self.sensors["main"] = sensor + self.shape = shape + self.fixture = fixture + + self.preSolve = function() end + self.postSolve = function() end + + -- Points all body, fixture and shape functions to this wf.Collider object + -- This means that the user can call collider:setLinearVelocity for instance without having to say collider.body:setLinearVelocity + for k, v in pairs(self.body.__index) do + if + k ~= "__gc" + and k ~= "__eq" + and k ~= "__index" + and k ~= "__tostring" + and k ~= "destroy" + and k ~= "type" + and k ~= "typeOf" + then + self[k] = function(self, ...) + return v(self.body, ...) + end + end + end + for k, v in pairs(self.fixture.__index) do + if + k ~= "__gc" + and k ~= "__eq" + and k ~= "__index" + and k ~= "__tostring" + and k ~= "destroy" + and k ~= "type" + and k ~= "typeOf" + then + self[k] = function(self, ...) + return v(self.fixture, ...) + end + end + end + for k, v in pairs(self.shape.__index) do + if + k ~= "__gc" + and k ~= "__eq" + and k ~= "__index" + and k ~= "__tostring" + and k ~= "destroy" + and k ~= "type" + and k ~= "typeOf" + then + self[k] = function(self, ...) + return v(self.shape, ...) + end + end + end + + return setmetatable(self, Collider) +end + +function Collider:collisionEventsClear() + self.collision_events = {} + for other, _ in pairs(self.world.collision_classes) do + self.collision_events[other] = {} + end +end + +function Collider:setCollisionClass(collision_class_name) + if not self.world.collision_classes[collision_class_name] then + error("Collision class " .. collision_class_name .. " doesn't exist.") + end + self.collision_class = collision_class_name + for _, fixture in pairs(self.fixtures) do + if self.world.masks[collision_class_name] then + fixture:setCategory(unpack(self.world.masks[collision_class_name].categories)) + fixture:setMask(unpack(self.world.masks[collision_class_name].masks)) + end + end +end + +function Collider:enter(other_collision_class_name) + local events = self.collision_events[other_collision_class_name] + if events and #events >= 1 then + for _, e in ipairs(events) do + if e.collision_type == "enter" then + if not self.collision_stay[other_collision_class_name] then + self.collision_stay[other_collision_class_name] = {} + end + table.insert( + self.collision_stay[other_collision_class_name], + { collider = e.collider_2, contact = e.contact } + ) + self.enter_collision_data[other_collision_class_name] = { collider = e.collider_2, contact = e.contact } + return true + end + end + end +end + +function Collider:getEnterCollisionData(other_collision_class_name) + return self.enter_collision_data[other_collision_class_name] +end + +function Collider:exit(other_collision_class_name) + local events = self.collision_events[other_collision_class_name] + if events and #events >= 1 then + for _, e in ipairs(events) do + if e.collision_type == "exit" then + if self.collision_stay[other_collision_class_name] then + for i = #self.collision_stay[other_collision_class_name], 1, -1 do + local collision_stay = self.collision_stay[other_collision_class_name][i] + if collision_stay.collider.id == e.collider_2.id then + table.remove(self.collision_stay[other_collision_class_name], i) + end + end + end + self.exit_collision_data[other_collision_class_name] = { collider = e.collider_2, contact = e.contact } + return true + end + end + end +end + +function Collider:getExitCollisionData(other_collision_class_name) + return self.exit_collision_data[other_collision_class_name] +end + +function Collider:stay(other_collision_class_name) + if self.collision_stay[other_collision_class_name] then + if #self.collision_stay[other_collision_class_name] >= 1 then + return true + end + end +end + +function Collider:getStayCollisionData(other_collision_class_name) + return self.collision_stay[other_collision_class_name] +end + +function Collider:setPreSolve(callback) + self.preSolve = callback +end + +function Collider:setPostSolve(callback) + self.postSolve = callback +end + +function Collider:setObject(object) + self.object = object +end + +function Collider:getObject() + return self.object +end + +function Collider:addShape(shape_name, shape_type, ...) + if self.shapes[shape_name] or self.fixtures[shape_name] then + error("Shape/fixture " .. shape_name .. " already exists.") + end + local args = { ... } + local shape = love.physics["new" .. shape_type](unpack(args)) + local fixture = love.physics.newFixture(self.body, shape) + if self.world.masks[self.collision_class] then + fixture:setCategory(unpack(self.world.masks[self.collision_class].categories)) + fixture:setMask(unpack(self.world.masks[self.collision_class].masks)) + end + fixture:setUserData(self) + local sensor = love.physics.newFixture(self.body, shape) + sensor:setSensor(true) + sensor:setUserData(self) + + self.shapes[shape_name] = shape + self.fixtures[shape_name] = fixture + self.sensors[shape_name] = sensor +end + +function Collider:removeShape(shape_name) + if not self.shapes[shape_name] then + return + end + self.shapes[shape_name] = nil + self.fixtures[shape_name]:setUserData(nil) + self.fixtures[shape_name]:destroy() + self.fixtures[shape_name] = nil + self.sensors[shape_name]:setUserData(nil) + self.sensors[shape_name]:destroy() + self.sensors[shape_name] = nil +end + +function Collider:destroy() + self.collision_stay = nil + self.enter_collision_data = nil + self.exit_collision_data = nil + self:collisionEventsClear() + + self:setObject(nil) + for name, _ in pairs(self.fixtures) do + self.shapes[name] = nil + self.fixtures[name]:setUserData(nil) + self.fixtures[name] = nil + self.sensors[name]:setUserData(nil) + self.sensors[name] = nil + end + self.body:destroy() + self.body = nil +end + +wf.World = World +wf.Collider = Collider + +return wf diff --git a/libs/windfield/mlib/mlib.lua b/libs/windfield/mlib/mlib.lua index 76067c6..2d70998 100644 --- a/libs/windfield/mlib/mlib.lua +++ b/libs/windfield/mlib/mlib.lua @@ -1,1152 +1,1317 @@ ---[[ License - A math library made in Lua - copyright (C) 2014 Davis Claiborne - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - Contact me at davisclaib@gmail.com -]] - --- Local Utility Functions ---------------------- {{{ -local unpack = table.unpack or unpack - --- Used to handle variable-argument functions and whether they are passed as func{ table } or func( unpack( table ) ) -local function checkInput( ... ) - local input = {} - if type( ... ) ~= 'table' then input = { ... } else input = ... end - return input -end - --- Deals with floats / verify false false values. This can happen because of significant figures. -local function checkFuzzy( number1, number2 ) - return ( number1 - .00001 <= number2 and number2 <= number1 + .00001 ) -end - --- Remove multiple occurrences from a table. -local function removeDuplicatePairs( tab ) - for index1 = #tab, 1, -1 do - local first = tab[index1] - for index2 = #tab, 1, -1 do - local second = tab[index2] - if index1 ~= index2 then - if type( first[1] ) == 'number' and type( second[1] ) == 'number' and type( first[2] ) == 'number' and type( second[2] ) == 'number' then - if checkFuzzy( first[1], second[1] ) and checkFuzzy( first[2], second[2] ) then - table.remove( tab, index1 ) - end - elseif first[1] == second[1] and first[2] == second[2] then - table.remove( tab, index1 ) - end - end - end - end - return tab -end - - -local function removeDuplicates4Points( tab ) - for index1 = #tab, 1, -1 do - local first = tab[index1] - for index2 = #tab, 1, -1 do - local second = tab[index2] - if index1 ~= index2 then - if type( first[1] ) ~= type( second[1] ) then return false end - if type( first[2] ) == 'number' and type( second[2] ) == 'number' and type( first[3] ) == 'number' and type( second[3] ) == 'number' then - if checkFuzzy( first[2], second[2] ) and checkFuzzy( first[3], second[3] ) then - table.remove( tab, index1 ) - end - elseif checkFuzzy( first[1], second[1] ) and checkFuzzy( first[2], second[2] ) and checkFuzzy( first[3], second[3] ) then - table.remove( tab, index1 ) - end - end - end - end - return tab -end - - --- Add points to the table. -local function addPoints( tab, x, y ) - tab[#tab + 1] = x - tab[#tab + 1] = y -end - --- Like removeDuplicatePairs but specifically for numbers in a flat table -local function removeDuplicatePointsFlat( tab ) - for i = #tab, 1 -2 do - for ii = #tab - 2, 3, -2 do - if i ~= ii then - local x1, y1 = tab[i], tab[i + 1] - local x2, y2 = tab[ii], tab[ii + 1] - if checkFuzzy( x1, x2 ) and checkFuzzy( y1, y2 ) then - table.remove( tab, ii ); table.remove( tab, ii + 1 ) - end - end - end - end - return tab -end - - --- Check if input is actually a number -local function validateNumber( n ) - if type( n ) ~= 'number' then return false - elseif n ~= n then return false -- nan - elseif math.abs( n ) == math.huge then return false - else return true end -end - -local function cycle( tab, index ) return tab[( index - 1 ) % #tab + 1] end - -local function getGreatestPoint( points, offset ) - offset = offset or 1 - local start = 2 - offset - local greatest = points[start] - local least = points[start] - for i = 2, #points / 2 do - i = i * 2 - offset - if points[i] > greatest then - greatest = points[i] - end - if points[i] < least then - least = points[i] - end - end - return greatest, least -end - -local function isWithinBounds( min, num, max ) - return num >= min and num <= max -end - -local function distance2( x1, y1, x2, y2 ) -- Faster since it does not use math.sqrt - local dx, dy = x1 - x2, y1 - y2 - return dx * dx + dy * dy -end -- }}} - --- Points -------------------------------------- {{{ -local function rotatePoint( x, y, rotation, ox, oy ) - ox, oy = ox or 0, oy or 0 - return ( x - ox ) * math.cos( rotation ) + ox - ( y - oy ) * math.sin( rotation ), ( x - ox ) * math.sin( rotation ) + ( y - oy ) * math.cos( rotation ) + oy -end - -local function scalePoint( x, y, scale, ox, oy ) - ox, oy = ox or 0, oy or 0 - return ( x - ox ) * scale + ox, ( y - oy ) * scale + oy -end --- }}} - --- Lines --------------------------------------- {{{ --- Returns the length of a line. -local function getLength( x1, y1, x2, y2 ) - local dx, dy = x1 - x2, y1 - y2 - return math.sqrt( dx * dx + dy * dy ) -end - --- Gives the midpoint of a line. -local function getMidpoint( x1, y1, x2, y2 ) - return ( x1 + x2 ) / 2, ( y1 + y2 ) / 2 -end - --- Gives the slope of a line. -local function getSlope( x1, y1, x2, y2 ) - if checkFuzzy( x1, x2 ) then return false end -- Technically it's undefined, but this is easier to program. - return ( y1 - y2 ) / ( x1 - x2 ) -end - --- Gives the perpendicular slope of a line. --- x1, y1, x2, y2 --- slope -local function getPerpendicularSlope( ... ) - local input = checkInput( ... ) - local slope - - if #input ~= 1 then - slope = getSlope( unpack( input ) ) - else - slope = unpack( input ) - end - - if not slope then return 0 -- Vertical lines become horizontal. - elseif checkFuzzy( slope, 0 ) then return false -- Horizontal lines become vertical. - else return -1 / slope end -end - --- Gives the y-intercept of a line. --- x1, y1, x2, y2 --- x1, y1, slope -local function getYIntercept( x, y, ... ) - local input = checkInput( ... ) - local slope - - if #input == 1 then - slope = input[1] - else - slope = getSlope( x, y, unpack( input ) ) - end - - if not slope then return x, true end -- This way we have some information on the line. - return y - slope * x, false -end - --- Gives the intersection of two lines. --- slope1, slope2, x1, y1, x2, y2 --- slope1, intercept1, slope2, intercept2 --- x1, y1, x2, y2, x3, y3, x4, y4 -local function getLineLineIntersection( ... ) - local input = checkInput( ... ) - local x1, y1, x2, y2, x3, y3, x4, y4 - local slope1, intercept1 - local slope2, intercept2 - local x, y - - if #input == 4 then -- Given slope1, intercept1, slope2, intercept2. - slope1, intercept1, slope2, intercept2 = unpack( input ) - - -- Since these are lines, not segments, we can use arbitrary points, such as ( 1, y ), ( 2, y ) - y1 = slope1 and slope1 * 1 + intercept1 or 1 - y2 = slope1 and slope1 * 2 + intercept1 or 2 - y3 = slope2 and slope2 * 1 + intercept2 or 1 - y4 = slope2 and slope2 * 2 + intercept2 or 2 - x1 = slope1 and ( y1 - intercept1 ) / slope1 or intercept1 - x2 = slope1 and ( y2 - intercept1 ) / slope1 or intercept1 - x3 = slope2 and ( y3 - intercept2 ) / slope2 or intercept2 - x4 = slope2 and ( y4 - intercept2 ) / slope2 or intercept2 - elseif #input == 6 then -- Given slope1, intercept1, and 2 points on the other line. - slope1, intercept1 = input[1], input[2] - slope2 = getSlope( input[3], input[4], input[5], input[6] ) - intercept2 = getYIntercept( input[3], input[4], input[5], input[6] ) - - y1 = slope1 and slope1 * 1 + intercept1 or 1 - y2 = slope1 and slope1 * 2 + intercept1 or 2 - y3 = input[4] - y4 = input[6] - x1 = slope1 and ( y1 - intercept1 ) / slope1 or intercept1 - x2 = slope1 and ( y2 - intercept1 ) / slope1 or intercept1 - x3 = input[3] - x4 = input[5] - elseif #input == 8 then -- Given 2 points on line 1 and 2 points on line 2. - slope1 = getSlope( input[1], input[2], input[3], input[4] ) - intercept1 = getYIntercept( input[1], input[2], input[3], input[4] ) - slope2 = getSlope( input[5], input[6], input[7], input[8] ) - intercept2 = getYIntercept( input[5], input[6], input[7], input[8] ) - - x1, y1, x2, y2, x3, y3, x4, y4 = unpack( input ) - end - - if not slope1 and not slope2 then -- Both are vertical lines - if x1 == x3 then -- Have to have the same x positions to intersect - return true - else - return false - end - elseif not slope1 then -- First is vertical - x = x1 -- They have to meet at this x, since it is this line's only x - y = slope2 and slope2 * x + intercept2 or 1 - elseif not slope2 then -- Second is vertical - x = x3 -- Vice-Versa - y = slope1 * x + intercept1 - elseif checkFuzzy( slope1, slope2 ) then -- Parallel (not vertical) - if checkFuzzy( intercept1, intercept2 ) then -- Same intercept - return true - else - return false - end - else -- Regular lines - x = ( -intercept1 + intercept2 ) / ( slope1 - slope2 ) - y = slope1 * x + intercept1 - end - - return x, y -end - --- Gives the closest point on a line to a point. --- perpendicularX, perpendicularY, x1, y1, x2, y2 --- perpendicularX, perpendicularY, slope, intercept -local function getClosestPoint( perpendicularX, perpendicularY, ... ) - local input = checkInput( ... ) - local x, y, x1, y1, x2, y2, slope, intercept - - if #input == 4 then -- Given perpendicularX, perpendicularY, x1, y1, x2, y2 - x1, y1, x2, y2 = unpack( input ) - slope = getSlope( x1, y1, x2, y2 ) - intercept = getYIntercept( x1, y1, x2, y2 ) - elseif #input == 2 then -- Given perpendicularX, perpendicularY, slope, intercept - slope, intercept = unpack( input ) - x1, y1 = 1, slope and slope * 1 + intercept or 1 -- Need x1 and y1 in case of vertical/horizontal lines. - end - - if not slope then -- Vertical line - x, y = x1, perpendicularY -- Closest point is always perpendicular. - elseif checkFuzzy( slope, 0 ) then -- Horizontal line - x, y = perpendicularX, y1 - else - local perpendicularSlope = getPerpendicularSlope( slope ) - local perpendicularIntercept = getYIntercept( perpendicularX, perpendicularY, perpendicularSlope ) - x, y = getLineLineIntersection( slope, intercept, perpendicularSlope, perpendicularIntercept ) - end - - return x, y -end - --- Gives the intersection of a line and a line segment. --- x1, y1, x2, y2, x3, y3, x4, y4 --- x1, y1, x2, y2, slope, intercept -local function getLineSegmentIntersection( x1, y1, x2, y2, ... ) - local input = checkInput( ... ) - - local slope1, intercept1, x, y, lineX1, lineY1, lineX2, lineY2 - local slope2, intercept2 = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) - - if #input == 2 then -- Given slope, intercept - slope1, intercept1 = input[1], input[2] - lineX1, lineY1 = 1, slope1 and slope1 + intercept1 - lineX2, lineY2 = 2, slope1 and slope1 * 2 + intercept1 - else -- Given x3, y3, x4, y4 - lineX1, lineY1, lineX2, lineY2 = unpack( input ) - slope1 = getSlope( unpack( input ) ) - intercept1 = getYIntercept( unpack( input ) ) - end - - if not slope1 and not slope2 then -- Vertical lines - if checkFuzzy( x1, lineX1 ) then - return x1, y1, x2, y2 - else - return false - end - elseif not slope1 then -- slope1 is vertical - x, y = input[1], slope2 * input[1] + intercept2 - elseif not slope2 then -- slope2 is vertical - x, y = x1, slope1 * x1 + intercept1 - else - x, y = getLineLineIntersection( slope1, intercept1, slope2, intercept2 ) - end - - local length1, length2, distance - if x == true then -- Lines are collinear. - return x1, y1, x2, y2 - elseif x then -- There is an intersection - length1, length2 = getLength( x1, y1, x, y ), getLength( x2, y2, x, y ) - distance = getLength( x1, y1, x2, y2 ) - else -- Lines are parallel but not collinear. - if checkFuzzy( intercept1, intercept2 ) then - return x1, y1, x2, y2 - else - return false - end - end - - if length1 <= distance and length2 <= distance then return x, y else return false end -end - --- Checks if a point is on a line. --- Does not support the format using slope because vertical lines would be impossible to check. -local function checkLinePoint( x, y, x1, y1, x2, y2 ) - local m = getSlope( x1, y1, x2, y2 ) - local b = getYIntercept( x1, y1, m ) - - if not m then -- Vertical - return checkFuzzy( x, x1 ) - end - return checkFuzzy( y, m * x + b ) -end -- }}} - --- Segment -------------------------------------- {{{ --- Gives the perpendicular bisector of a line. -local function getPerpendicularBisector( x1, y1, x2, y2 ) - local slope = getSlope( x1, y1, x2, y2 ) - local midpointX, midpointY = getMidpoint( x1, y1, x2, y2 ) - return midpointX, midpointY, getPerpendicularSlope( slope ) -end - --- Gives whether or not a point lies on a line segment. -local function checkSegmentPoint( px, py, x1, y1, x2, y2 ) - -- Explanation around 5:20: https://www.youtube.com/watch?v=A86COO8KC58 - local x = checkLinePoint( px, py, x1, y1, x2, y2 ) - if not x then return false end - - local lengthX = x2 - x1 - local lengthY = y2 - y1 - - if checkFuzzy( lengthX, 0 ) then -- Vertical line - if checkFuzzy( px, x1 ) then - local low, high - if y1 > y2 then low = y2; high = y1 - else low = y1; high = y2 end - - if py >= low and py <= high then return true - else return false end - else - return false - end - elseif checkFuzzy( lengthY, 0 ) then -- Horizontal line - if checkFuzzy( py, y1 ) then - local low, high - if x1 > x2 then low = x2; high = x1 - else low = x1; high = x2 end - - if px >= low and px <= high then return true - else return false end - else - return false - end - end - - local distanceToPointX = ( px - x1 ) - local distanceToPointY = ( py - y1 ) - local scaleX = distanceToPointX / lengthX - local scaleY = distanceToPointY / lengthY - - if ( scaleX >= 0 and scaleX <= 1 ) and ( scaleY >= 0 and scaleY <= 1 ) then -- Intersection - return true - end - return false -end - --- Gives the point of intersection between two line segments. -local function getSegmentSegmentIntersection( x1, y1, x2, y2, x3, y3, x4, y4 ) - local slope1, intercept1 = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) - local slope2, intercept2 = getSlope( x3, y3, x4, y4 ), getYIntercept( x3, y3, x4, y4 ) - - if ( ( slope1 and slope2 ) and checkFuzzy( slope1, slope2 ) ) or ( not slope1 and not slope2 ) then -- Parallel lines - if checkFuzzy( intercept1, intercept2 ) then -- The same lines, possibly in different points. - local points = {} - if checkSegmentPoint( x1, y1, x3, y3, x4, y4 ) then addPoints( points, x1, y1 ) end - if checkSegmentPoint( x2, y2, x3, y3, x4, y4 ) then addPoints( points, x2, y2 ) end - if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then addPoints( points, x3, y3 ) end - if checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then addPoints( points, x4, y4 ) end - - points = removeDuplicatePointsFlat( points ) - if #points == 0 then return false end - return unpack( points ) - else - return false - end - end - - local x, y = getLineLineIntersection( x1, y1, x2, y2, x3, y3, x4, y4 ) - if x and checkSegmentPoint( x, y, x1, y1, x2, y2 ) and checkSegmentPoint( x, y, x3, y3, x4, y4 ) then - return x, y - end - return false -end -- }}} - --- Math ----------------------------------------- {{{ --- Get the root of a number (i.e. the 2nd (square) root of 4 is 2) -local function getRoot( number, root ) - return number ^ ( 1 / root ) -end - --- Checks if a number is prime. -local function isPrime( number ) - if number < 2 then return false end - - for i = 2, math.sqrt( number ) do - if number % i == 0 then - return false - end - end - return true -end - --- Rounds a number to the xth decimal place (round( 3.14159265359, 4 ) --> 3.1416) -local function round( number, place ) - local pow = 10 ^ ( place or 0 ) - return math.floor( number * pow + .5 ) / pow -end - --- Gives the summation given a local function -local function getSummation( start, stop, func ) - local returnValues = {} - local sum = 0 - for i = start, stop do - local value = func( i, returnValues ) - returnValues[i] = value - sum = sum + value - end - return sum -end - --- Gives the percent of change. -local function getPercentOfChange( old, new ) - if old == 0 and new == 0 then - return 0 - else - return ( new - old ) / math.abs( old ) - end -end - --- Gives the percentage of a number. -local function getPercentage( percent, number ) - return percent * number -end - --- Returns the quadratic roots of an equation. -local function getQuadraticRoots( a, b, c ) - local discriminant = b ^ 2 - ( 4 * a * c ) - if discriminant < 0 then return false end - discriminant = math.sqrt( discriminant ) - local denominator = ( 2 * a ) - return ( -b - discriminant ) / denominator, ( -b + discriminant ) / denominator -end - --- Gives the angle between three points. -local function getAngle( x1, y1, x2, y2, x3, y3 ) - local a = getLength( x3, y3, x2, y2 ) - local b = getLength( x1, y1, x2, y2 ) - local c = getLength( x1, y1, x3, y3 ) - - return math.acos( ( a * a + b * b - c * c ) / ( 2 * a * b ) ) -end -- }}} - --- Circle --------------------------------------- {{{ --- Gives the area of the circle. -local function getCircleArea( radius ) - return math.pi * ( radius * radius ) -end - --- Checks if a point is within the radius of a circle. -local function checkCirclePoint( x, y, circleX, circleY, radius ) - return getLength( circleX, circleY, x, y ) <= radius -end - --- Checks if a point is on a circle. -local function isPointOnCircle( x, y, circleX, circleY, radius ) - return checkFuzzy( getLength( circleX, circleY, x, y ), radius ) -end - --- Gives the circumference of a circle. -local function getCircumference( radius ) - return 2 * math.pi * radius -end - --- Gives the intersection of a line and a circle. -local function getCircleLineIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) - slope = getSlope( x1, y1, x2, y2 ) - intercept = getYIntercept( x1, y1, slope ) - - if slope then - local a = ( 1 + slope ^ 2 ) - local b = ( -2 * ( circleX ) + ( 2 * slope * intercept ) - ( 2 * circleY * slope ) ) - local c = ( circleX ^ 2 + intercept ^ 2 - 2 * ( circleY ) * ( intercept ) + circleY ^ 2 - radius ^ 2 ) - - x1, x2 = getQuadraticRoots( a, b, c ) - - if not x1 then return false end - - y1 = slope * x1 + intercept - y2 = slope * x2 + intercept - - if checkFuzzy( x1, x2 ) and checkFuzzy( y1, y2 ) then - return 'tangent', x1, y1 - else - return 'secant', x1, y1, x2, y2 - end - else -- Vertical Lines - local lengthToPoint1 = circleX - x1 - local remainingDistance = lengthToPoint1 - radius - local intercept = math.sqrt( -( lengthToPoint1 ^ 2 - radius ^ 2 ) ) - - if -( lengthToPoint1 ^ 2 - radius ^ 2 ) < 0 then return false end - - local bottomX, bottomY = x1, circleY - intercept - local topX, topY = x1, circleY + intercept - - if topY ~= bottomY then - return 'secant', topX, topY, bottomX, bottomY - else - return 'tangent', topX, topY - end - end -end - --- Gives the type of intersection of a line segment. -local function getCircleSegmentIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) - local Type, x3, y3, x4, y4 = getCircleLineIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) - if not Type then return false end - - local slope, intercept = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) - - if isPointOnCircle( x1, y1, circleX, circleY, radius ) and isPointOnCircle( x2, y2, circleX, circleY, radius ) then -- Both points are on line-segment. - return 'chord', x1, y1, x2, y2 - end - - if slope then - if checkCirclePoint( x1, y1, circleX, circleY, radius ) and checkCirclePoint( x2, y2, circleX, circleY, radius ) then -- Line-segment is fully in circle. - return 'enclosed', x1, y1, x2, y2 - elseif x3 and x4 then - if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) and not checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then -- Only the first of the points is on the line-segment. - return 'tangent', x3, y3 - elseif checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) and not checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then -- Only the second of the points is on the line-segment. - return 'tangent', x4, y4 - else -- Neither of the points are on the circle (means that the segment is not on the circle, but "encasing" the circle) - if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) and checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then - return 'secant', x3, y3, x4, y4 - else - return false - end - end - elseif not x4 then -- Is a tangent. - if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then - return 'tangent', x3, y3 - else -- Neither of the points are on the line-segment (means that the segment is not on the circle or "encasing" the circle). - local length = getLength( x1, y1, x2, y2 ) - local distance1 = getLength( x1, y1, x3, y3 ) - local distance2 = getLength( x2, y2, x3, y3 ) - - if length > distance1 or length > distance2 then - return false - elseif length < distance1 and length < distance2 then - return false - else - return 'tangent', x3, y3 - end - end - end - else - local lengthToPoint1 = circleX - x1 - local remainingDistance = lengthToPoint1 - radius - local intercept = math.sqrt( -( lengthToPoint1 ^ 2 - radius ^ 2 ) ) - - if -( lengthToPoint1 ^ 2 - radius ^ 2 ) < 0 then return false end - - local topX, topY = x1, circleY - intercept - local bottomX, bottomY = x1, circleY + intercept - - local length = getLength( x1, y1, x2, y2 ) - local distance1 = getLength( x1, y1, topX, topY ) - local distance2 = getLength( x2, y2, topX, topY ) - - if bottomY ~= topY then -- Not a tangent - if checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) and checkSegmentPoint( bottomX, bottomY, x1, y1, x2, y2 ) then - return 'chord', topX, topY, bottomX, bottomY - elseif checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) then - return 'tangent', topX, topY - elseif checkSegmentPoint( bottomX, bottomY, x1, y1, x2, y2 ) then - return 'tangent', bottomX, bottomY - else - return false - end - else -- Tangent - if checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) then - return 'tangent', topX, topY - else - return false - end - end - end -end - --- Checks if one circle intersects another circle. -local function getCircleCircleIntersection( circle1x, circle1y, radius1, circle2x, circle2y, radius2 ) - local length = getLength( circle1x, circle1y, circle2x, circle2y ) - if length > radius1 + radius2 then return false end -- If the distance is greater than the two radii, they can't intersect. - if checkFuzzy( length, 0 ) and checkFuzzy( radius1, radius2 ) then return 'equal' end - if checkFuzzy( circle1x, circle2x ) and checkFuzzy( circle1y, circle2y ) then return 'collinear' end - - local a = ( radius1 * radius1 - radius2 * radius2 + length * length ) / ( 2 * length ) - local h = math.sqrt( radius1 * radius1 - a * a ) - - local p2x = circle1x + a * ( circle2x - circle1x ) / length - local p2y = circle1y + a * ( circle2y - circle1y ) / length - local p3x = p2x + h * ( circle2y - circle1y ) / length - local p3y = p2y - h * ( circle2x - circle1x ) / length - local p4x = p2x - h * ( circle2y - circle1y ) / length - local p4y = p2y + h * ( circle2x - circle1x ) / length - - if not validateNumber( p3x ) or not validateNumber( p3y ) or not validateNumber( p4x ) or not validateNumber( p4y ) then - return 'inside' - end - - if checkFuzzy( length, radius1 + radius2 ) or checkFuzzy( length, math.abs( radius1 - radius2 ) ) then return 'tangent', p3x, p3y end - return 'intersection', p3x, p3y, p4x, p4y -end - --- Checks if circle1 is entirely inside of circle2. -local function isCircleCompletelyInsideCircle( circle1x, circle1y, circle1radius, circle2x, circle2y, circle2radius ) - if not checkCirclePoint( circle1x, circle1y, circle2x, circle2y, circle2radius ) then return false end - local Type = getCircleCircleIntersection( circle2x, circle2y, circle2radius, circle1x, circle1y, circle1radius ) - if ( Type ~= 'tangent' and Type ~= 'collinear' and Type ~= 'inside' ) then return false end - return true -end - --- Checks if a line-segment is entirely within a circle. -local function isSegmentCompletelyInsideCircle( circleX, circleY, circleRadius, x1, y1, x2, y2 ) - local Type = getCircleSegmentIntersection( circleX, circleY, circleRadius, x1, y1, x2, y2 ) - return Type == 'enclosed' -end -- }}} - --- Polygon -------------------------------------- {{{ --- Gives the signed area. --- If the points are clockwise the number is negative, otherwise, it's positive. -local function getSignedPolygonArea( ... ) - local points = checkInput( ... ) - - -- Shoelace formula (https://en.wikipedia.org/wiki/Shoelace_formula). - points[#points + 1] = points[1] - points[#points + 1] = points[2] - - return ( .5 * getSummation( 1, #points / 2, - function( index ) - index = index * 2 - 1 -- Convert it to work properly. - return ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) - end - ) ) -end - --- Simply returns the area of the polygon. -local function getPolygonArea( ... ) - return math.abs( getSignedPolygonArea( ... ) ) -end - --- Gives the height of a triangle, given the base. --- base, x1, y1, x2, y2, x3, y3, x4, y4 --- base, area -local function getTriangleHeight( base, ... ) - local input = checkInput( ... ) - local area - - if #input == 1 then area = input[1] -- Given area. - else area = getPolygonArea( input ) end -- Given coordinates. - - return ( 2 * area ) / base, area -end - --- Gives the centroid of the polygon. -local function getCentroid( ... ) - local points = checkInput( ... ) - - points[#points + 1] = points[1] - points[#points + 1] = points[2] - - local area = getSignedPolygonArea( points ) -- Needs to be signed here in case points are counter-clockwise. - - -- This formula: https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon - local centroidX = ( 1 / ( 6 * area ) ) * ( getSummation( 1, #points / 2, - function( index ) - index = index * 2 - 1 -- Convert it to work properly. - return ( ( points[index] + cycle( points, index + 2 ) ) * ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) ) - end - ) ) - - local centroidY = ( 1 / ( 6 * area ) ) * ( getSummation( 1, #points / 2, - function( index ) - index = index * 2 - 1 -- Convert it to work properly. - return ( ( points[index + 1] + cycle( points, index + 3 ) ) * ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) ) - end - ) ) - - return centroidX, centroidY -end - --- Returns whether or not a line intersects a polygon. --- x1, y1, x2, y2, polygonPoints -local function getPolygonLineIntersection( x1, y1, x2, y2, ... ) - local input = checkInput( ... ) - local choices = {} - - local slope = getSlope( x1, y1, x2, y2 ) - local intercept = getYIntercept( x1, y1, slope ) - - local x3, y3, x4, y4 - if slope then - x3, x4 = 1, 2 - y3, y4 = slope * x3 + intercept, slope * x4 + intercept - else - x3, x4 = x1, x1 - y3, y4 = y1, y2 - end - - for i = 1, #input, 2 do - local x1, y1, x2, y2 = getLineSegmentIntersection( input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ), x3, y3, x4, y4 ) - if x1 and not x2 then choices[#choices + 1] = { x1, y1 } - elseif x1 and x2 then choices[#choices + 1] = { x1, y1, x2, y2 } end - -- No need to check 2-point sets since they only intersect each poly line once. - end - - local final = removeDuplicatePairs( choices ) - return #final > 0 and final or false -end - --- Returns if the line segment intersects the polygon. --- x1, y1, x2, y2, polygonPoints -local function getPolygonSegmentIntersection( x1, y1, x2, y2, ... ) - local input = checkInput( ... ) - local choices = {} - - for i = 1, #input, 2 do - local x1, y1, x2, y2 = getSegmentSegmentIntersection( input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ), x1, y1, x2, y2 ) - if x1 and not x2 then choices[#choices + 1] = { x1, y1 } - elseif x2 then choices[#choices + 1] = { x1, y1, x2, y2 } end - end - - local final = removeDuplicatePairs( choices ) - return #final > 0 and final or false -end - --- Checks if the point lies INSIDE the polygon not on the polygon. -local function checkPolygonPoint( px, py, ... ) - local points = { unpack( checkInput( ... ) ) } -- Make a new table, as to not edit values of previous. - - local greatest, least = getGreatestPoint( points, 0 ) - if not isWithinBounds( least, py, greatest ) then return false end - greatest, least = getGreatestPoint( points ) - if not isWithinBounds( least, px, greatest ) then return false end - - local count = 0 - for i = 1, #points, 2 do - if checkFuzzy( points[i + 1], py ) then - points[i + 1] = py + .001 -- Handles vertices that lie on the point. - -- Not exactly mathematically correct, but a lot easier. - end - if points[i + 3] and checkFuzzy( points[i + 3], py ) then - points[i + 3] = py + .001 -- Do not need to worry about alternate case, since points[2] has already been done. - end - local x1, y1 = points[i], points[i + 1] - local x2, y2 = points[i + 2] or points[1], points[i + 3] or points[2] - - if getSegmentSegmentIntersection( px, py, greatest, py, x1, y1, x2, y2 ) then - count = count + 1 - end - end - - return count and count % 2 ~= 0 -end - --- Returns if the line segment is fully or partially inside. --- x1, y1, x2, y2, polygonPoints -local function isSegmentInsidePolygon( x1, y1, x2, y2, ... ) - local input = checkInput( ... ) - - local choices = getPolygonSegmentIntersection( x1, y1, x2, y2, input ) -- If it's partially enclosed that's all we need. - if choices then return true end - - if checkPolygonPoint( x1, y1, input ) or checkPolygonPoint( x2, y2, input ) then return true end - return false -end - --- Returns whether two polygons intersect. -local function getPolygonPolygonIntersection( polygon1, polygon2 ) - local choices = {} - - for index1 = 1, #polygon1, 2 do - local intersections = getPolygonSegmentIntersection( polygon1[index1], polygon1[index1 + 1], cycle( polygon1, index1 + 2 ), cycle( polygon1, index1 + 3 ), polygon2 ) - if intersections then - for index2 = 1, #intersections do - choices[#choices + 1] = intersections[index2] - end - end - end - - for index1 = 1, #polygon2, 2 do - local intersections = getPolygonSegmentIntersection( polygon2[index1], polygon2[index1 + 1], cycle( polygon2, index1 + 2 ), cycle( polygon2, index1 + 3 ), polygon1 ) - if intersections then - for index2 = 1, #intersections do - choices[#choices + 1] = intersections[index2] - end - end - end - - choices = removeDuplicatePairs( choices ) - for i = #choices, 1, -1 do - if type( choices[i][1] ) == 'table' then -- Remove co-linear pairs. - table.remove( choices, i ) - end - end - - return #choices > 0 and choices -end - --- Returns whether the circle intersects the polygon. --- x, y, radius, polygonPoints -local function getPolygonCircleIntersection( x, y, radius, ... ) - local input = checkInput( ... ) - local choices = {} - - for i = 1, #input, 2 do - local Type, x1, y1, x2, y2 = getCircleSegmentIntersection( x, y, radius, input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ) ) - if x2 then - choices[#choices + 1] = { Type, x1, y1, x2, y2 } - elseif x1 then choices[#choices + 1] = { Type, x1, y1 } end - end - - local final = removeDuplicates4Points( choices ) - - return #final > 0 and final -end - --- Returns whether the circle is inside the polygon. --- x, y, radius, polygonPoints -local function isCircleInsidePolygon( x, y, radius, ... ) - local input = checkInput( ... ) - return checkPolygonPoint( x, y, input ) -end - --- Returns whether the polygon is inside the polygon. -local function isPolygonInsidePolygon( polygon1, polygon2 ) - local bool = false - for i = 1, #polygon2, 2 do - local result = false - result = isSegmentInsidePolygon( polygon2[i], polygon2[i + 1], cycle( polygon2, i + 2 ), cycle( polygon2, i + 3 ), polygon1 ) - if result then bool = true; break end - end - return bool -end - --- Checks if a segment is completely inside a polygon -local function isSegmentCompletelyInsidePolygon( x1, y1, x2, y2, ... ) - local polygon = checkInput( ... ) - if not checkPolygonPoint( x1, y1, polygon ) - or not checkPolygonPoint( x2, y2, polygon ) - or getPolygonSegmentIntersection( x1, y1, x2, y2, polygon ) then - return false - end - return true -end - --- Checks if a polygon is completely inside another polygon -local function isPolygonCompletelyInsidePolygon( polygon1, polygon2 ) - for i = 1, #polygon1, 2 do - local x1, y1 = polygon1[i], polygon1[i + 1] - local x2, y2 = polygon1[i + 2] or polygon1[1], polygon1[i + 3] or polygon1[2] - if not isSegmentCompletelyInsidePolygon( x1, y1, x2, y2, polygon2 ) then - return false - end - end - return true -end - --------------- Circle w/ Polygons -------------- --- Gets if a polygon is completely within a circle --- circleX, circleY, circleRadius, polygonPoints -local function isPolygonCompletelyInsideCircle( circleX, circleY, circleRadius, ... ) - local input = checkInput( ... ) - local function isDistanceLess( px, py, x, y, circleRadius ) -- Faster, does not use math.sqrt - local distanceX, distanceY = px - x, py - y - return distanceX * distanceX + distanceY * distanceY < circleRadius * circleRadius -- Faster. For comparing distances only. - end - - for i = 1, #input, 2 do - if not checkCirclePoint( input[i], input[i + 1], circleX, circleY, circleRadius ) then return false end - end - return true -end - --- Checks if a circle is completely within a polygon --- circleX, circleY, circleRadius, polygonPoints -local function isCircleCompletelyInsidePolygon( circleX, circleY, circleRadius, ... ) - local input = checkInput( ... ) - if not checkPolygonPoint( circleX, circleY, ... ) then return false end - - local rad2 = circleRadius * circleRadius - - for i = 1, #input, 2 do - local x1, y1 = input[i], input[i + 1] - local x2, y2 = input[i + 2] or input[1], input[i + 3] or input[2] - if distance2( x1, y1, circleX, circleY ) <= rad2 then return false end - if getCircleSegmentIntersection( circleX, circleY, circleRadius, x1, y1, x2, y2 ) then return false end - end - return true -end -- }}} - --- Statistics ----------------------------------- {{{ --- Gets the average of a list of points --- points -local function getMean( ... ) - local input = checkInput( ... ) - - mean = getSummation( 1, #input, - function( i, t ) - return input[i] - end - ) / #input - - return mean -end - -local function getMedian( ... ) - local input = checkInput( ... ) - - table.sort( input ) - - local median - if #input % 2 == 0 then -- If you have an even number of terms, you need to get the average of the middle 2. - median = getMean( input[#input / 2], input[#input / 2 + 1] ) - else - median = input[#input / 2 + .5] - end - - return median -end - --- Gets the mode of a number. -local function getMode( ... ) - local input = checkInput( ... ) - - table.sort( input ) - local sorted = {} - for i = 1, #input do - local value = input[i] - sorted[value] = sorted[value] and sorted[value] + 1 or 1 - end - - local occurrences, least = 0, {} - for i, value in pairs( sorted ) do - if value > occurrences then - least = { i } - occurrences = value - elseif value == occurrences then - least[#least + 1] = i - end - end - - if #least >= 1 then return least, occurrences - else return false end -end - --- Gets the range of the numbers. -local function getRange( ... ) - local input = checkInput( ... ) - local high, low = math.max( unpack( input ) ), math.min( unpack( input ) ) - return high - low -end - --- Gets the variance of a set of numbers. -local function getVariance( ... ) - local input = checkInput( ... ) - local mean = getMean( ... ) - local sum = 0 - for i = 1, #input do - sum = sum + ( mean - input[i] ) * ( mean - input[i] ) - end - return sum / #input -end - --- Gets the standard deviation of a set of numbers. -local function getStandardDeviation( ... ) - return math.sqrt( getVariance( ... ) ) -end - --- Gets the central tendency of a set of numbers. -local function getCentralTendency( ... ) - local mode, occurrences = getMode( ... ) - return mode, occurrences, getMedian( ... ), getMean( ... ) -end - --- Gets the variation ratio of a data set. -local function getVariationRatio( ... ) - local input = checkInput( ... ) - local numbers, times = getMode( ... ) - times = times * #numbers -- Account for bimodal data - return 1 - ( times / #input ) -end - --- Gets the measures of dispersion of a data set. -local function getDispersion( ... ) - return getVariationRatio( ... ), getRange( ... ), getStandardDeviation( ... ) -end -- }}} - -return { - _VERSION = 'MLib 0.10.0', - _DESCRIPTION = 'A math and shape-intersection detection library for Lua', - _URL = 'https://github.com/davisdude/mlib', - point = { - rotate = rotatePoint, - scale = scalePoint, - }, - line = { - getLength = getLength, - getMidpoint = getMidpoint, - getSlope = getSlope, - getPerpendicularSlope = getPerpendicularSlope, - getYIntercept = getYIntercept, - getIntersection = getLineLineIntersection, - getClosestPoint = getClosestPoint, - getSegmentIntersection = getLineSegmentIntersection, - checkPoint = checkLinePoint, - - -- Aliases - getDistance = getLength, - getCircleIntersection = getCircleLineIntersection, - getPolygonIntersection = getPolygonLineIntersection, - getLineIntersection = getLineLineIntersection, - }, - segment = { - checkPoint = checkSegmentPoint, - getPerpendicularBisector = getPerpendicularBisector, - getIntersection = getSegmentSegmentIntersection, - - -- Aliases - getCircleIntersection = getCircleSegmentIntersection, - getPolygonIntersection = getPolygonSegmentIntersection, - getLineIntersection = getLineSegmentIntersection, - getSegmentIntersection = getSegmentSegmentIntersection, - isSegmentCompletelyInsideCircle = isSegmentCompletelyInsideCircle, - isSegmentCompletelyInsidePolygon = isSegmentCompletelyInsidePolygon, - }, - math = { - getRoot = getRoot, - isPrime = isPrime, - round = round, - getSummation = getSummation, - getPercentOfChange = getPercentOfChange, - getPercentage = getPercentage, - getQuadraticRoots = getQuadraticRoots, - getAngle = getAngle, - }, - circle = { - getArea = getCircleArea, - checkPoint = checkCirclePoint, - isPointOnCircle = isPointOnCircle, - getCircumference = getCircumference, - getLineIntersection = getCircleLineIntersection, - getSegmentIntersection = getCircleSegmentIntersection, - getCircleIntersection = getCircleCircleIntersection, - isCircleCompletelyInside = isCircleCompletelyInsideCircle, - isPolygonCompletelyInside = isPolygonCompletelyInsideCircle, - isSegmentCompletelyInside = isSegmentCompletelyInsideCircle, - - -- Aliases - getPolygonIntersection = getPolygonCircleIntersection, - isCircleInsidePolygon = isCircleInsidePolygon, - isCircleCompletelyInsidePolygon = isCircleCompletelyInsidePolygon, - }, - polygon = { - getSignedArea = getSignedPolygonArea, - getArea = getPolygonArea, - getTriangleHeight = getTriangleHeight, - getCentroid = getCentroid, - getLineIntersection = getPolygonLineIntersection, - getSegmentIntersection = getPolygonSegmentIntersection, - checkPoint = checkPolygonPoint, - isSegmentInside = isSegmentInsidePolygon, - getPolygonIntersection = getPolygonPolygonIntersection, - getCircleIntersection = getPolygonCircleIntersection, - isCircleInside = isCircleInsidePolygon, - isPolygonInside = isPolygonInsidePolygon, - isCircleCompletelyInside = isCircleCompletelyInsidePolygon, - isSegmentCompletelyInside = isSegmentCompletelyInsidePolygon, - isPolygonCompletelyInside = isPolygonCompletelyInsidePolygon, - - -- Aliases - isCircleCompletelyOver = isPolygonCompletelyInsideCircle, - }, - statistics = { - getMean = getMean, - getMedian = getMedian, - getMode = getMode, - getRange = getRange, - getVariance = getVariance, - getStandardDeviation = getStandardDeviation, - getCentralTendency = getCentralTendency, - getVariationRatio = getVariationRatio, - getDispersion = getDispersion, - }, -} +--[[ License + A math library made in Lua + copyright (C) 2014 Davis Claiborne + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + Contact me at davisclaib@gmail.com +]] + +-- Local Utility Functions ---------------------- {{{ +local unpack = table.unpack or unpack + +-- Used to handle variable-argument functions and whether they are passed as func{ table } or func( unpack( table ) ) +local function checkInput(...) + local input = {} + if type(...) ~= "table" then + input = { ... } + else + input = ... + end + return input +end + +-- Deals with floats / verify false false values. This can happen because of significant figures. +local function checkFuzzy(number1, number2) + return (number1 - 0.00001 <= number2 and number2 <= number1 + 0.00001) +end + +-- Remove multiple occurrences from a table. +local function removeDuplicatePairs(tab) + for index1 = #tab, 1, -1 do + local first = tab[index1] + for index2 = #tab, 1, -1 do + local second = tab[index2] + if index1 ~= index2 then + if + type(first[1]) == "number" + and type(second[1]) == "number" + and type(first[2]) == "number" + and type(second[2]) == "number" + then + if checkFuzzy(first[1], second[1]) and checkFuzzy(first[2], second[2]) then + table.remove(tab, index1) + end + elseif first[1] == second[1] and first[2] == second[2] then + table.remove(tab, index1) + end + end + end + end + return tab +end + +local function removeDuplicates4Points(tab) + for index1 = #tab, 1, -1 do + local first = tab[index1] + for index2 = #tab, 1, -1 do + local second = tab[index2] + if index1 ~= index2 then + if type(first[1]) ~= type(second[1]) then + return false + end + if + type(first[2]) == "number" + and type(second[2]) == "number" + and type(first[3]) == "number" + and type(second[3]) == "number" + then + if checkFuzzy(first[2], second[2]) and checkFuzzy(first[3], second[3]) then + table.remove(tab, index1) + end + elseif + checkFuzzy(first[1], second[1]) + and checkFuzzy(first[2], second[2]) + and checkFuzzy(first[3], second[3]) + then + table.remove(tab, index1) + end + end + end + end + return tab +end + +-- Add points to the table. +local function addPoints(tab, x, y) + tab[#tab + 1] = x + tab[#tab + 1] = y +end + +-- Like removeDuplicatePairs but specifically for numbers in a flat table +local function removeDuplicatePointsFlat(tab) + for i = #tab, 1 - 2 do + for ii = #tab - 2, 3, -2 do + if i ~= ii then + local x1, y1 = tab[i], tab[i + 1] + local x2, y2 = tab[ii], tab[ii + 1] + if checkFuzzy(x1, x2) and checkFuzzy(y1, y2) then + table.remove(tab, ii) + table.remove(tab, ii + 1) + end + end + end + end + return tab +end + +-- Check if input is actually a number +local function validateNumber(n) + if type(n) ~= "number" then + return false + elseif n ~= n then + return false -- nan + elseif math.abs(n) == math.huge then + return false + else + return true + end +end + +local function cycle(tab, index) + return tab[(index - 1) % #tab + 1] +end + +local function getGreatestPoint(points, offset) + offset = offset or 1 + local start = 2 - offset + local greatest = points[start] + local least = points[start] + for i = 2, #points / 2 do + i = i * 2 - offset + if points[i] > greatest then + greatest = points[i] + end + if points[i] < least then + least = points[i] + end + end + return greatest, least +end + +local function isWithinBounds(min, num, max) + return num >= min and num <= max +end + +local function distance2(x1, y1, x2, y2) -- Faster since it does not use math.sqrt + local dx, dy = x1 - x2, y1 - y2 + return dx * dx + dy * dy +end -- }}} + +-- Points -------------------------------------- {{{ +local function rotatePoint(x, y, rotation, ox, oy) + ox, oy = ox or 0, oy or 0 + return (x - ox) * math.cos(rotation) + ox - (y - oy) * math.sin(rotation), + (x - ox) * math.sin(rotation) + (y - oy) * math.cos(rotation) + oy +end + +local function scalePoint(x, y, scale, ox, oy) + ox, oy = ox or 0, oy or 0 + return (x - ox) * scale + ox, (y - oy) * scale + oy +end +-- }}} + +-- Lines --------------------------------------- {{{ +-- Returns the length of a line. +local function getLength(x1, y1, x2, y2) + local dx, dy = x1 - x2, y1 - y2 + return math.sqrt(dx * dx + dy * dy) +end + +-- Gives the midpoint of a line. +local function getMidpoint(x1, y1, x2, y2) + return (x1 + x2) / 2, (y1 + y2) / 2 +end + +-- Gives the slope of a line. +local function getSlope(x1, y1, x2, y2) + if checkFuzzy(x1, x2) then + return false + end -- Technically it's undefined, but this is easier to program. + return (y1 - y2) / (x1 - x2) +end + +-- Gives the perpendicular slope of a line. +-- x1, y1, x2, y2 +-- slope +local function getPerpendicularSlope(...) + local input = checkInput(...) + local slope + + if #input ~= 1 then + slope = getSlope(unpack(input)) + else + slope = unpack(input) + end + + if not slope then + return 0 -- Vertical lines become horizontal. + elseif checkFuzzy(slope, 0) then + return false -- Horizontal lines become vertical. + else + return -1 / slope + end +end + +-- Gives the y-intercept of a line. +-- x1, y1, x2, y2 +-- x1, y1, slope +local function getYIntercept(x, y, ...) + local input = checkInput(...) + local slope + + if #input == 1 then + slope = input[1] + else + slope = getSlope(x, y, unpack(input)) + end + + if not slope then + return x, true + end -- This way we have some information on the line. + return y - slope * x, false +end + +-- Gives the intersection of two lines. +-- slope1, slope2, x1, y1, x2, y2 +-- slope1, intercept1, slope2, intercept2 +-- x1, y1, x2, y2, x3, y3, x4, y4 +local function getLineLineIntersection(...) + local input = checkInput(...) + local x1, y1, x2, y2, x3, y3, x4, y4 + local slope1, intercept1 + local slope2, intercept2 + local x, y + + if #input == 4 then -- Given slope1, intercept1, slope2, intercept2. + slope1, intercept1, slope2, intercept2 = unpack(input) + + -- Since these are lines, not segments, we can use arbitrary points, such as ( 1, y ), ( 2, y ) + y1 = slope1 and slope1 * 1 + intercept1 or 1 + y2 = slope1 and slope1 * 2 + intercept1 or 2 + y3 = slope2 and slope2 * 1 + intercept2 or 1 + y4 = slope2 and slope2 * 2 + intercept2 or 2 + x1 = slope1 and (y1 - intercept1) / slope1 or intercept1 + x2 = slope1 and (y2 - intercept1) / slope1 or intercept1 + x3 = slope2 and (y3 - intercept2) / slope2 or intercept2 + x4 = slope2 and (y4 - intercept2) / slope2 or intercept2 + elseif #input == 6 then -- Given slope1, intercept1, and 2 points on the other line. + slope1, intercept1 = input[1], input[2] + slope2 = getSlope(input[3], input[4], input[5], input[6]) + intercept2 = getYIntercept(input[3], input[4], input[5], input[6]) + + y1 = slope1 and slope1 * 1 + intercept1 or 1 + y2 = slope1 and slope1 * 2 + intercept1 or 2 + y3 = input[4] + y4 = input[6] + x1 = slope1 and (y1 - intercept1) / slope1 or intercept1 + x2 = slope1 and (y2 - intercept1) / slope1 or intercept1 + x3 = input[3] + x4 = input[5] + elseif #input == 8 then -- Given 2 points on line 1 and 2 points on line 2. + slope1 = getSlope(input[1], input[2], input[3], input[4]) + intercept1 = getYIntercept(input[1], input[2], input[3], input[4]) + slope2 = getSlope(input[5], input[6], input[7], input[8]) + intercept2 = getYIntercept(input[5], input[6], input[7], input[8]) + + x1, y1, x2, y2, x3, y3, x4, y4 = unpack(input) + end + + if not slope1 and not slope2 then -- Both are vertical lines + if x1 == x3 then -- Have to have the same x positions to intersect + return true + else + return false + end + elseif not slope1 then -- First is vertical + x = x1 -- They have to meet at this x, since it is this line's only x + y = slope2 and slope2 * x + intercept2 or 1 + elseif not slope2 then -- Second is vertical + x = x3 -- Vice-Versa + y = slope1 * x + intercept1 + elseif checkFuzzy(slope1, slope2) then -- Parallel (not vertical) + if checkFuzzy(intercept1, intercept2) then -- Same intercept + return true + else + return false + end + else -- Regular lines + x = (-intercept1 + intercept2) / (slope1 - slope2) + y = slope1 * x + intercept1 + end + + return x, y +end + +-- Gives the closest point on a line to a point. +-- perpendicularX, perpendicularY, x1, y1, x2, y2 +-- perpendicularX, perpendicularY, slope, intercept +local function getClosestPoint(perpendicularX, perpendicularY, ...) + local input = checkInput(...) + local x, y, x1, y1, x2, y2, slope, intercept + + if #input == 4 then -- Given perpendicularX, perpendicularY, x1, y1, x2, y2 + x1, y1, x2, y2 = unpack(input) + slope = getSlope(x1, y1, x2, y2) + intercept = getYIntercept(x1, y1, x2, y2) + elseif #input == 2 then -- Given perpendicularX, perpendicularY, slope, intercept + slope, intercept = unpack(input) + x1, y1 = 1, slope and slope * 1 + intercept or 1 -- Need x1 and y1 in case of vertical/horizontal lines. + end + + if not slope then -- Vertical line + x, y = x1, perpendicularY -- Closest point is always perpendicular. + elseif checkFuzzy(slope, 0) then -- Horizontal line + x, y = perpendicularX, y1 + else + local perpendicularSlope = getPerpendicularSlope(slope) + local perpendicularIntercept = getYIntercept(perpendicularX, perpendicularY, perpendicularSlope) + x, y = getLineLineIntersection(slope, intercept, perpendicularSlope, perpendicularIntercept) + end + + return x, y +end + +-- Gives the intersection of a line and a line segment. +-- x1, y1, x2, y2, x3, y3, x4, y4 +-- x1, y1, x2, y2, slope, intercept +local function getLineSegmentIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + + local slope1, intercept1, x, y, lineX1, lineY1, lineX2, lineY2 + local slope2, intercept2 = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + + if #input == 2 then -- Given slope, intercept + slope1, intercept1 = input[1], input[2] + lineX1, lineY1 = 1, slope1 and slope1 + intercept1 + lineX2, lineY2 = 2, slope1 and slope1 * 2 + intercept1 + else -- Given x3, y3, x4, y4 + lineX1, lineY1, lineX2, lineY2 = unpack(input) + slope1 = getSlope(unpack(input)) + intercept1 = getYIntercept(unpack(input)) + end + + if not slope1 and not slope2 then -- Vertical lines + if checkFuzzy(x1, lineX1) then + return x1, y1, x2, y2 + else + return false + end + elseif not slope1 then -- slope1 is vertical + x, y = input[1], slope2 * input[1] + intercept2 + elseif not slope2 then -- slope2 is vertical + x, y = x1, slope1 * x1 + intercept1 + else + x, y = getLineLineIntersection(slope1, intercept1, slope2, intercept2) + end + + local length1, length2, distance + if x == true then -- Lines are collinear. + return x1, y1, x2, y2 + elseif x then -- There is an intersection + length1, length2 = getLength(x1, y1, x, y), getLength(x2, y2, x, y) + distance = getLength(x1, y1, x2, y2) + else -- Lines are parallel but not collinear. + if checkFuzzy(intercept1, intercept2) then + return x1, y1, x2, y2 + else + return false + end + end + + if length1 <= distance and length2 <= distance then + return x, y + else + return false + end +end + +-- Checks if a point is on a line. +-- Does not support the format using slope because vertical lines would be impossible to check. +local function checkLinePoint(x, y, x1, y1, x2, y2) + local m = getSlope(x1, y1, x2, y2) + local b = getYIntercept(x1, y1, m) + + if not m then -- Vertical + return checkFuzzy(x, x1) + end + return checkFuzzy(y, m * x + b) +end -- }}} + +-- Segment -------------------------------------- {{{ +-- Gives the perpendicular bisector of a line. +local function getPerpendicularBisector(x1, y1, x2, y2) + local slope = getSlope(x1, y1, x2, y2) + local midpointX, midpointY = getMidpoint(x1, y1, x2, y2) + return midpointX, midpointY, getPerpendicularSlope(slope) +end + +-- Gives whether or not a point lies on a line segment. +local function checkSegmentPoint(px, py, x1, y1, x2, y2) + -- Explanation around 5:20: https://www.youtube.com/watch?v=A86COO8KC58 + local x = checkLinePoint(px, py, x1, y1, x2, y2) + if not x then + return false + end + + local lengthX = x2 - x1 + local lengthY = y2 - y1 + + if checkFuzzy(lengthX, 0) then -- Vertical line + if checkFuzzy(px, x1) then + local low, high + if y1 > y2 then + low = y2 + high = y1 + else + low = y1 + high = y2 + end + + if py >= low and py <= high then + return true + else + return false + end + else + return false + end + elseif checkFuzzy(lengthY, 0) then -- Horizontal line + if checkFuzzy(py, y1) then + local low, high + if x1 > x2 then + low = x2 + high = x1 + else + low = x1 + high = x2 + end + + if px >= low and px <= high then + return true + else + return false + end + else + return false + end + end + + local distanceToPointX = (px - x1) + local distanceToPointY = (py - y1) + local scaleX = distanceToPointX / lengthX + local scaleY = distanceToPointY / lengthY + + if (scaleX >= 0 and scaleX <= 1) and (scaleY >= 0 and scaleY <= 1) then -- Intersection + return true + end + return false +end + +-- Gives the point of intersection between two line segments. +local function getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4) + local slope1, intercept1 = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + local slope2, intercept2 = getSlope(x3, y3, x4, y4), getYIntercept(x3, y3, x4, y4) + + if ((slope1 and slope2) and checkFuzzy(slope1, slope2)) or (not slope1 and not slope2) then -- Parallel lines + if checkFuzzy(intercept1, intercept2) then -- The same lines, possibly in different points. + local points = {} + if checkSegmentPoint(x1, y1, x3, y3, x4, y4) then + addPoints(points, x1, y1) + end + if checkSegmentPoint(x2, y2, x3, y3, x4, y4) then + addPoints(points, x2, y2) + end + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) then + addPoints(points, x3, y3) + end + if checkSegmentPoint(x4, y4, x1, y1, x2, y2) then + addPoints(points, x4, y4) + end + + points = removeDuplicatePointsFlat(points) + if #points == 0 then + return false + end + return unpack(points) + else + return false + end + end + + local x, y = getLineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) + if x and checkSegmentPoint(x, y, x1, y1, x2, y2) and checkSegmentPoint(x, y, x3, y3, x4, y4) then + return x, y + end + return false +end -- }}} + +-- Math ----------------------------------------- {{{ +-- Get the root of a number (i.e. the 2nd (square) root of 4 is 2) +local function getRoot(number, root) + return number ^ (1 / root) +end + +-- Checks if a number is prime. +local function isPrime(number) + if number < 2 then + return false + end + + for i = 2, math.sqrt(number) do + if number % i == 0 then + return false + end + end + return true +end + +-- Rounds a number to the xth decimal place (round( 3.14159265359, 4 ) --> 3.1416) +local function round(number, place) + local pow = 10 ^ (place or 0) + return math.floor(number * pow + 0.5) / pow +end + +-- Gives the summation given a local function +local function getSummation(start, stop, func) + local returnValues = {} + local sum = 0 + for i = start, stop do + local value = func(i, returnValues) + returnValues[i] = value + sum = sum + value + end + return sum +end + +-- Gives the percent of change. +local function getPercentOfChange(old, new) + if old == 0 and new == 0 then + return 0 + else + return (new - old) / math.abs(old) + end +end + +-- Gives the percentage of a number. +local function getPercentage(percent, number) + return percent * number +end + +-- Returns the quadratic roots of an equation. +local function getQuadraticRoots(a, b, c) + local discriminant = b ^ 2 - (4 * a * c) + if discriminant < 0 then + return false + end + discriminant = math.sqrt(discriminant) + local denominator = (2 * a) + return (-b - discriminant) / denominator, (-b + discriminant) / denominator +end + +-- Gives the angle between three points. +local function getAngle(x1, y1, x2, y2, x3, y3) + local a = getLength(x3, y3, x2, y2) + local b = getLength(x1, y1, x2, y2) + local c = getLength(x1, y1, x3, y3) + + return math.acos((a * a + b * b - c * c) / (2 * a * b)) +end -- }}} + +-- Circle --------------------------------------- {{{ +-- Gives the area of the circle. +local function getCircleArea(radius) + return math.pi * (radius * radius) +end + +-- Checks if a point is within the radius of a circle. +local function checkCirclePoint(x, y, circleX, circleY, radius) + return getLength(circleX, circleY, x, y) <= radius +end + +-- Checks if a point is on a circle. +local function isPointOnCircle(x, y, circleX, circleY, radius) + return checkFuzzy(getLength(circleX, circleY, x, y), radius) +end + +-- Gives the circumference of a circle. +local function getCircumference(radius) + return 2 * math.pi * radius +end + +-- Gives the intersection of a line and a circle. +local function getCircleLineIntersection(circleX, circleY, radius, x1, y1, x2, y2) + slope = getSlope(x1, y1, x2, y2) + intercept = getYIntercept(x1, y1, slope) + + if slope then + local a = (1 + slope ^ 2) + local b = (-2 * circleX + (2 * slope * intercept) - (2 * circleY * slope)) + local c = (circleX ^ 2 + intercept ^ 2 - 2 * circleY * intercept + circleY ^ 2 - radius ^ 2) + + x1, x2 = getQuadraticRoots(a, b, c) + + if not x1 then + return false + end + + y1 = slope * x1 + intercept + y2 = slope * x2 + intercept + + if checkFuzzy(x1, x2) and checkFuzzy(y1, y2) then + return "tangent", x1, y1 + else + return "secant", x1, y1, x2, y2 + end + else -- Vertical Lines + local lengthToPoint1 = circleX - x1 + local remainingDistance = lengthToPoint1 - radius + local intercept = math.sqrt(-(lengthToPoint1 ^ 2 - radius ^ 2)) + + if -(lengthToPoint1 ^ 2 - radius ^ 2) < 0 then + return false + end + + local bottomX, bottomY = x1, circleY - intercept + local topX, topY = x1, circleY + intercept + + if topY ~= bottomY then + return "secant", topX, topY, bottomX, bottomY + else + return "tangent", topX, topY + end + end +end + +-- Gives the type of intersection of a line segment. +local function getCircleSegmentIntersection(circleX, circleY, radius, x1, y1, x2, y2) + local Type, x3, y3, x4, y4 = getCircleLineIntersection(circleX, circleY, radius, x1, y1, x2, y2) + if not Type then + return false + end + + local slope, intercept = getSlope(x1, y1, x2, y2), getYIntercept(x1, y1, x2, y2) + + if isPointOnCircle(x1, y1, circleX, circleY, radius) and isPointOnCircle(x2, y2, circleX, circleY, radius) then -- Both points are on line-segment. + return "chord", x1, y1, x2, y2 + end + + if slope then + if + checkCirclePoint(x1, y1, circleX, circleY, radius) and checkCirclePoint(x2, y2, circleX, circleY, radius) + then -- Line-segment is fully in circle. + return "enclosed", x1, y1, x2, y2 + elseif x3 and x4 then + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) and not checkSegmentPoint(x4, y4, x1, y1, x2, y2) then -- Only the first of the points is on the line-segment. + return "tangent", x3, y3 + elseif checkSegmentPoint(x4, y4, x1, y1, x2, y2) and not checkSegmentPoint(x3, y3, x1, y1, x2, y2) then -- Only the second of the points is on the line-segment. + return "tangent", x4, y4 + else -- Neither of the points are on the circle (means that the segment is not on the circle, but "encasing" the circle) + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) and checkSegmentPoint(x4, y4, x1, y1, x2, y2) then + return "secant", x3, y3, x4, y4 + else + return false + end + end + elseif not x4 then -- Is a tangent. + if checkSegmentPoint(x3, y3, x1, y1, x2, y2) then + return "tangent", x3, y3 + else -- Neither of the points are on the line-segment (means that the segment is not on the circle or "encasing" the circle). + local length = getLength(x1, y1, x2, y2) + local distance1 = getLength(x1, y1, x3, y3) + local distance2 = getLength(x2, y2, x3, y3) + + if length > distance1 or length > distance2 then + return false + elseif length < distance1 and length < distance2 then + return false + else + return "tangent", x3, y3 + end + end + end + else + local lengthToPoint1 = circleX - x1 + local remainingDistance = lengthToPoint1 - radius + local intercept = math.sqrt(-(lengthToPoint1 ^ 2 - radius ^ 2)) + + if -(lengthToPoint1 ^ 2 - radius ^ 2) < 0 then + return false + end + + local topX, topY = x1, circleY - intercept + local bottomX, bottomY = x1, circleY + intercept + + local length = getLength(x1, y1, x2, y2) + local distance1 = getLength(x1, y1, topX, topY) + local distance2 = getLength(x2, y2, topX, topY) + + if bottomY ~= topY then -- Not a tangent + if + checkSegmentPoint(topX, topY, x1, y1, x2, y2) and checkSegmentPoint(bottomX, bottomY, x1, y1, x2, y2) + then + return "chord", topX, topY, bottomX, bottomY + elseif checkSegmentPoint(topX, topY, x1, y1, x2, y2) then + return "tangent", topX, topY + elseif checkSegmentPoint(bottomX, bottomY, x1, y1, x2, y2) then + return "tangent", bottomX, bottomY + else + return false + end + else -- Tangent + if checkSegmentPoint(topX, topY, x1, y1, x2, y2) then + return "tangent", topX, topY + else + return false + end + end + end +end + +-- Checks if one circle intersects another circle. +local function getCircleCircleIntersection(circle1x, circle1y, radius1, circle2x, circle2y, radius2) + local length = getLength(circle1x, circle1y, circle2x, circle2y) + if length > radius1 + radius2 then + return false + end -- If the distance is greater than the two radii, they can't intersect. + if checkFuzzy(length, 0) and checkFuzzy(radius1, radius2) then + return "equal" + end + if checkFuzzy(circle1x, circle2x) and checkFuzzy(circle1y, circle2y) then + return "collinear" + end + + local a = (radius1 * radius1 - radius2 * radius2 + length * length) / (2 * length) + local h = math.sqrt(radius1 * radius1 - a * a) + + local p2x = circle1x + a * (circle2x - circle1x) / length + local p2y = circle1y + a * (circle2y - circle1y) / length + local p3x = p2x + h * (circle2y - circle1y) / length + local p3y = p2y - h * (circle2x - circle1x) / length + local p4x = p2x - h * (circle2y - circle1y) / length + local p4y = p2y + h * (circle2x - circle1x) / length + + if not validateNumber(p3x) or not validateNumber(p3y) or not validateNumber(p4x) or not validateNumber(p4y) then + return "inside" + end + + if checkFuzzy(length, radius1 + radius2) or checkFuzzy(length, math.abs(radius1 - radius2)) then + return "tangent", p3x, p3y + end + return "intersection", p3x, p3y, p4x, p4y +end + +-- Checks if circle1 is entirely inside of circle2. +local function isCircleCompletelyInsideCircle(circle1x, circle1y, circle1radius, circle2x, circle2y, circle2radius) + if not checkCirclePoint(circle1x, circle1y, circle2x, circle2y, circle2radius) then + return false + end + local Type = getCircleCircleIntersection(circle2x, circle2y, circle2radius, circle1x, circle1y, circle1radius) + if Type ~= "tangent" and Type ~= "collinear" and Type ~= "inside" then + return false + end + return true +end + +-- Checks if a line-segment is entirely within a circle. +local function isSegmentCompletelyInsideCircle(circleX, circleY, circleRadius, x1, y1, x2, y2) + local Type = getCircleSegmentIntersection(circleX, circleY, circleRadius, x1, y1, x2, y2) + return Type == "enclosed" +end -- }}} + +-- Polygon -------------------------------------- {{{ +-- Gives the signed area. +-- If the points are clockwise the number is negative, otherwise, it's positive. +local function getSignedPolygonArea(...) + local points = checkInput(...) + + -- Shoelace formula (https://en.wikipedia.org/wiki/Shoelace_formula). + points[#points + 1] = points[1] + points[#points + 1] = points[2] + + return ( + 0.5 + * getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1])) + end) + ) +end + +-- Simply returns the area of the polygon. +local function getPolygonArea(...) + return math.abs(getSignedPolygonArea(...)) +end + +-- Gives the height of a triangle, given the base. +-- base, x1, y1, x2, y2, x3, y3, x4, y4 +-- base, area +local function getTriangleHeight(base, ...) + local input = checkInput(...) + local area + + if #input == 1 then + area = input[1] -- Given area. + else + area = getPolygonArea(input) + end -- Given coordinates. + + return (2 * area) / base, area +end + +-- Gives the centroid of the polygon. +local function getCentroid(...) + local points = checkInput(...) + + points[#points + 1] = points[1] + points[#points + 1] = points[2] + + local area = getSignedPolygonArea(points) -- Needs to be signed here in case points are counter-clockwise. + + -- This formula: https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon + local centroidX = (1 / (6 * area)) + * ( + getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ( + (points[index] + cycle(points, index + 2)) + * ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1])) + ) + end) + ) + + local centroidY = (1 / (6 * area)) + * ( + getSummation(1, #points / 2, function(index) + index = index * 2 - 1 -- Convert it to work properly. + return ( + (points[index + 1] + cycle(points, index + 3)) + * ((points[index] * cycle(points, index + 3)) - (cycle(points, index + 2) * points[index + 1])) + ) + end) + ) + + return centroidX, centroidY +end + +-- Returns whether or not a line intersects a polygon. +-- x1, y1, x2, y2, polygonPoints +local function getPolygonLineIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + local choices = {} + + local slope = getSlope(x1, y1, x2, y2) + local intercept = getYIntercept(x1, y1, slope) + + local x3, y3, x4, y4 + if slope then + x3, x4 = 1, 2 + y3, y4 = slope * x3 + intercept, slope * x4 + intercept + else + x3, x4 = x1, x1 + y3, y4 = y1, y2 + end + + for i = 1, #input, 2 do + local x1, y1, x2, y2 = + getLineSegmentIntersection(input[i], input[i + 1], cycle(input, i + 2), cycle(input, i + 3), x3, y3, x4, y4) + if x1 and not x2 then + choices[#choices + 1] = { x1, y1 } + elseif x1 and x2 then + choices[#choices + 1] = { x1, y1, x2, y2 } + end + -- No need to check 2-point sets since they only intersect each poly line once. + end + + local final = removeDuplicatePairs(choices) + return #final > 0 and final or false +end + +-- Returns if the line segment intersects the polygon. +-- x1, y1, x2, y2, polygonPoints +local function getPolygonSegmentIntersection(x1, y1, x2, y2, ...) + local input = checkInput(...) + local choices = {} + + for i = 1, #input, 2 do + local x1, y1, x2, y2 = getSegmentSegmentIntersection( + input[i], + input[i + 1], + cycle(input, i + 2), + cycle(input, i + 3), + x1, + y1, + x2, + y2 + ) + if x1 and not x2 then + choices[#choices + 1] = { x1, y1 } + elseif x2 then + choices[#choices + 1] = { x1, y1, x2, y2 } + end + end + + local final = removeDuplicatePairs(choices) + return #final > 0 and final or false +end + +-- Checks if the point lies INSIDE the polygon not on the polygon. +local function checkPolygonPoint(px, py, ...) + local points = { unpack(checkInput(...)) } -- Make a new table, as to not edit values of previous. + + local greatest, least = getGreatestPoint(points, 0) + if not isWithinBounds(least, py, greatest) then + return false + end + greatest, least = getGreatestPoint(points) + if not isWithinBounds(least, px, greatest) then + return false + end + + local count = 0 + for i = 1, #points, 2 do + if checkFuzzy(points[i + 1], py) then + points[i + 1] = py + 0.001 -- Handles vertices that lie on the point. + -- Not exactly mathematically correct, but a lot easier. + end + if points[i + 3] and checkFuzzy(points[i + 3], py) then + points[i + 3] = py + 0.001 -- Do not need to worry about alternate case, since points[2] has already been done. + end + local x1, y1 = points[i], points[i + 1] + local x2, y2 = points[i + 2] or points[1], points[i + 3] or points[2] + + if getSegmentSegmentIntersection(px, py, greatest, py, x1, y1, x2, y2) then + count = count + 1 + end + end + + return count and count % 2 ~= 0 +end + +-- Returns if the line segment is fully or partially inside. +-- x1, y1, x2, y2, polygonPoints +local function isSegmentInsidePolygon(x1, y1, x2, y2, ...) + local input = checkInput(...) + + local choices = getPolygonSegmentIntersection(x1, y1, x2, y2, input) -- If it's partially enclosed that's all we need. + if choices then + return true + end + + if checkPolygonPoint(x1, y1, input) or checkPolygonPoint(x2, y2, input) then + return true + end + return false +end + +-- Returns whether two polygons intersect. +local function getPolygonPolygonIntersection(polygon1, polygon2) + local choices = {} + + for index1 = 1, #polygon1, 2 do + local intersections = getPolygonSegmentIntersection( + polygon1[index1], + polygon1[index1 + 1], + cycle(polygon1, index1 + 2), + cycle(polygon1, index1 + 3), + polygon2 + ) + if intersections then + for index2 = 1, #intersections do + choices[#choices + 1] = intersections[index2] + end + end + end + + for index1 = 1, #polygon2, 2 do + local intersections = getPolygonSegmentIntersection( + polygon2[index1], + polygon2[index1 + 1], + cycle(polygon2, index1 + 2), + cycle(polygon2, index1 + 3), + polygon1 + ) + if intersections then + for index2 = 1, #intersections do + choices[#choices + 1] = intersections[index2] + end + end + end + + choices = removeDuplicatePairs(choices) + for i = #choices, 1, -1 do + if type(choices[i][1]) == "table" then -- Remove co-linear pairs. + table.remove(choices, i) + end + end + + return #choices > 0 and choices +end + +-- Returns whether the circle intersects the polygon. +-- x, y, radius, polygonPoints +local function getPolygonCircleIntersection(x, y, radius, ...) + local input = checkInput(...) + local choices = {} + + for i = 1, #input, 2 do + local Type, x1, y1, x2, y2 = + getCircleSegmentIntersection(x, y, radius, input[i], input[i + 1], cycle(input, i + 2), cycle(input, i + 3)) + if x2 then + choices[#choices + 1] = { Type, x1, y1, x2, y2 } + elseif x1 then + choices[#choices + 1] = { Type, x1, y1 } + end + end + + local final = removeDuplicates4Points(choices) + + return #final > 0 and final +end + +-- Returns whether the circle is inside the polygon. +-- x, y, radius, polygonPoints +local function isCircleInsidePolygon(x, y, radius, ...) + local input = checkInput(...) + return checkPolygonPoint(x, y, input) +end + +-- Returns whether the polygon is inside the polygon. +local function isPolygonInsidePolygon(polygon1, polygon2) + local bool = false + for i = 1, #polygon2, 2 do + local result = false + result = isSegmentInsidePolygon( + polygon2[i], + polygon2[i + 1], + cycle(polygon2, i + 2), + cycle(polygon2, i + 3), + polygon1 + ) + if result then + bool = true + break + end + end + return bool +end + +-- Checks if a segment is completely inside a polygon +local function isSegmentCompletelyInsidePolygon(x1, y1, x2, y2, ...) + local polygon = checkInput(...) + if + not checkPolygonPoint(x1, y1, polygon) + or not checkPolygonPoint(x2, y2, polygon) + or getPolygonSegmentIntersection(x1, y1, x2, y2, polygon) + then + return false + end + return true +end + +-- Checks if a polygon is completely inside another polygon +local function isPolygonCompletelyInsidePolygon(polygon1, polygon2) + for i = 1, #polygon1, 2 do + local x1, y1 = polygon1[i], polygon1[i + 1] + local x2, y2 = polygon1[i + 2] or polygon1[1], polygon1[i + 3] or polygon1[2] + if not isSegmentCompletelyInsidePolygon(x1, y1, x2, y2, polygon2) then + return false + end + end + return true +end + +-------------- Circle w/ Polygons -------------- +-- Gets if a polygon is completely within a circle +-- circleX, circleY, circleRadius, polygonPoints +local function isPolygonCompletelyInsideCircle(circleX, circleY, circleRadius, ...) + local input = checkInput(...) + local function isDistanceLess(px, py, x, y, circleRadius) -- Faster, does not use math.sqrt + local distanceX, distanceY = px - x, py - y + return distanceX * distanceX + distanceY * distanceY < circleRadius * circleRadius -- Faster. For comparing distances only. + end + + for i = 1, #input, 2 do + if not checkCirclePoint(input[i], input[i + 1], circleX, circleY, circleRadius) then + return false + end + end + return true +end + +-- Checks if a circle is completely within a polygon +-- circleX, circleY, circleRadius, polygonPoints +local function isCircleCompletelyInsidePolygon(circleX, circleY, circleRadius, ...) + local input = checkInput(...) + if not checkPolygonPoint(circleX, circleY, ...) then + return false + end + + local rad2 = circleRadius * circleRadius + + for i = 1, #input, 2 do + local x1, y1 = input[i], input[i + 1] + local x2, y2 = input[i + 2] or input[1], input[i + 3] or input[2] + if distance2(x1, y1, circleX, circleY) <= rad2 then + return false + end + if getCircleSegmentIntersection(circleX, circleY, circleRadius, x1, y1, x2, y2) then + return false + end + end + return true +end -- }}} + +-- Statistics ----------------------------------- {{{ +-- Gets the average of a list of points +-- points +local function getMean(...) + local input = checkInput(...) + + mean = getSummation(1, #input, function(i, t) + return input[i] + end) / #input + + return mean +end + +local function getMedian(...) + local input = checkInput(...) + + table.sort(input) + + local median + if #input % 2 == 0 then -- If you have an even number of terms, you need to get the average of the middle 2. + median = getMean(input[#input / 2], input[#input / 2 + 1]) + else + median = input[#input / 2 + 0.5] + end + + return median +end + +-- Gets the mode of a number. +local function getMode(...) + local input = checkInput(...) + + table.sort(input) + local sorted = {} + for i = 1, #input do + local value = input[i] + sorted[value] = sorted[value] and sorted[value] + 1 or 1 + end + + local occurrences, least = 0, {} + for i, value in pairs(sorted) do + if value > occurrences then + least = { i } + occurrences = value + elseif value == occurrences then + least[#least + 1] = i + end + end + + if #least >= 1 then + return least, occurrences + else + return false + end +end + +-- Gets the range of the numbers. +local function getRange(...) + local input = checkInput(...) + local high, low = math.max(unpack(input)), math.min(unpack(input)) + return high - low +end + +-- Gets the variance of a set of numbers. +local function getVariance(...) + local input = checkInput(...) + local mean = getMean(...) + local sum = 0 + for i = 1, #input do + sum = sum + (mean - input[i]) * (mean - input[i]) + end + return sum / #input +end + +-- Gets the standard deviation of a set of numbers. +local function getStandardDeviation(...) + return math.sqrt(getVariance(...)) +end + +-- Gets the central tendency of a set of numbers. +local function getCentralTendency(...) + local mode, occurrences = getMode(...) + return mode, occurrences, getMedian(...), getMean(...) +end + +-- Gets the variation ratio of a data set. +local function getVariationRatio(...) + local input = checkInput(...) + local numbers, times = getMode(...) + times = times * #numbers -- Account for bimodal data + return 1 - (times / #input) +end + +-- Gets the measures of dispersion of a data set. +local function getDispersion(...) + return getVariationRatio(...), getRange(...), getStandardDeviation(...) +end -- }}} + +return { + _VERSION = "MLib 0.10.0", + _DESCRIPTION = "A math and shape-intersection detection library for Lua", + _URL = "https://github.com/davisdude/mlib", + point = { + rotate = rotatePoint, + scale = scalePoint, + }, + line = { + getLength = getLength, + getMidpoint = getMidpoint, + getSlope = getSlope, + getPerpendicularSlope = getPerpendicularSlope, + getYIntercept = getYIntercept, + getIntersection = getLineLineIntersection, + getClosestPoint = getClosestPoint, + getSegmentIntersection = getLineSegmentIntersection, + checkPoint = checkLinePoint, + + -- Aliases + getDistance = getLength, + getCircleIntersection = getCircleLineIntersection, + getPolygonIntersection = getPolygonLineIntersection, + getLineIntersection = getLineLineIntersection, + }, + segment = { + checkPoint = checkSegmentPoint, + getPerpendicularBisector = getPerpendicularBisector, + getIntersection = getSegmentSegmentIntersection, + + -- Aliases + getCircleIntersection = getCircleSegmentIntersection, + getPolygonIntersection = getPolygonSegmentIntersection, + getLineIntersection = getLineSegmentIntersection, + getSegmentIntersection = getSegmentSegmentIntersection, + isSegmentCompletelyInsideCircle = isSegmentCompletelyInsideCircle, + isSegmentCompletelyInsidePolygon = isSegmentCompletelyInsidePolygon, + }, + math = { + getRoot = getRoot, + isPrime = isPrime, + round = round, + getSummation = getSummation, + getPercentOfChange = getPercentOfChange, + getPercentage = getPercentage, + getQuadraticRoots = getQuadraticRoots, + getAngle = getAngle, + }, + circle = { + getArea = getCircleArea, + checkPoint = checkCirclePoint, + isPointOnCircle = isPointOnCircle, + getCircumference = getCircumference, + getLineIntersection = getCircleLineIntersection, + getSegmentIntersection = getCircleSegmentIntersection, + getCircleIntersection = getCircleCircleIntersection, + isCircleCompletelyInside = isCircleCompletelyInsideCircle, + isPolygonCompletelyInside = isPolygonCompletelyInsideCircle, + isSegmentCompletelyInside = isSegmentCompletelyInsideCircle, + + -- Aliases + getPolygonIntersection = getPolygonCircleIntersection, + isCircleInsidePolygon = isCircleInsidePolygon, + isCircleCompletelyInsidePolygon = isCircleCompletelyInsidePolygon, + }, + polygon = { + getSignedArea = getSignedPolygonArea, + getArea = getPolygonArea, + getTriangleHeight = getTriangleHeight, + getCentroid = getCentroid, + getLineIntersection = getPolygonLineIntersection, + getSegmentIntersection = getPolygonSegmentIntersection, + checkPoint = checkPolygonPoint, + isSegmentInside = isSegmentInsidePolygon, + getPolygonIntersection = getPolygonPolygonIntersection, + getCircleIntersection = getPolygonCircleIntersection, + isCircleInside = isCircleInsidePolygon, + isPolygonInside = isPolygonInsidePolygon, + isCircleCompletelyInside = isCircleCompletelyInsidePolygon, + isSegmentCompletelyInside = isSegmentCompletelyInsidePolygon, + isPolygonCompletelyInside = isPolygonCompletelyInsidePolygon, + + -- Aliases + isCircleCompletelyOver = isPolygonCompletelyInsideCircle, + }, + statistics = { + getMean = getMean, + getMedian = getMedian, + getMode = getMode, + getRange = getRange, + getVariance = getVariance, + getStandardDeviation = getStandardDeviation, + getCentralTendency = getCentralTendency, + getVariationRatio = getVariationRatio, + getDispersion = getDispersion, + }, +} diff --git a/main.lua b/main.lua index f7a790f..63705fd 100644 --- a/main.lua +++ b/main.lua @@ -1,10 +1,14 @@ -require 'constants' +require("constants") function love.run() - if love.load then love.load(love.arg.parseGameArguments(arg), arg) end + if love.load then + love.load(love.arg.parseGameArguments(arg), arg) + end -- We don't want the first frame's dt to include time taken by love.load. - if love.timer then love.timer.step() end + if love.timer then + love.timer.step() + end local dt = 0 @@ -13,32 +17,40 @@ function love.run() -- Process events. if love.event then love.event.pump() - for name, a,b,c,d,e,f in love.event.poll() do + for name, a, b, c, d, e, f in love.event.poll() do if name == "quit" then if not love.quit or not love.quit() then return a or 0 end end - love.handlers[name](a,b,c,d,e,f) + love.handlers[name](a, b, c, d, e, f) end end -- Update dt, as we'll be passing it to update - if love.timer then dt = love.timer.step() end + if love.timer then + dt = love.timer.step() + end -- Call update and draw - if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled + if love.update then + love.update(dt) + end -- will pass 0 if love.timer is disabled if love.graphics and love.graphics.isActive() then love.graphics.origin() love.graphics.clear(love.graphics.getBackgroundColor()) - if love.draw then love.draw() end + if love.draw then + love.draw() + end love.graphics.present() end - if love.timer then love.timer.sleep(0.001) end + if love.timer then + love.timer.sleep(0.001) + end end end @@ -92,7 +104,7 @@ function love.load() --Bullet lists Bullets1 = {} Bullets2 = {} - + DebugFlag = false EnableKeyPress1 = true KeyPressTime1 = 0 @@ -112,12 +124,11 @@ function love.load() musicStory = love.audio.newSource("music/story.mp3", "stream") musicPause = musicBattle:clone() - musicPause:setFilter{ - type = 'lowpass', - volume = 0.3, + musicPause:setFilter({ + type = "lowpass", + volume = 0.7, highgain = 0.4, - } - + }) end function love.keypressed(key) @@ -130,7 +141,6 @@ function love.keypressed(key) if _G.GAMESTATE == "GAME" then GameKeyPressed(key) end - end function love.update(dt) @@ -162,6 +172,11 @@ function love.update(dt) love.audio.play(musicPause) end end + if _G.GAMESTATE == "WIN" then + print(P1_WIN) + print(P2_WIN) + love.event.quit() + end end function love.draw() @@ -179,5 +194,4 @@ function love.draw() love.graphics.print("" .. GAMESTATE, 200, 200) end end - end diff --git a/mapsloader.lua b/mapsloader.lua index f1c9f40..d29c1d6 100644 --- a/mapsloader.lua +++ b/mapsloader.lua @@ -1,4 +1,4 @@ -function loadMap(lvl) +function loadMap(lvl) local mapfilelocation = "maps/" local extention = ".lua" local mapname = mapfilelocation .. "map" .. lvl .. extention @@ -13,4 +13,4 @@ function loadMap(lvl) Walls[#Walls]:setCollisionClass("Wall") end end -end \ No newline at end of file +end diff --git a/player.lua b/player.lua index b5e7dee..79cd0f6 100644 --- a/player.lua +++ b/player.lua @@ -47,79 +47,53 @@ function Player:shoot(bulletSpeed) return newBullet end -function handleKeys(dt) +function Player:handleKeys(up, down, left, right, dt) + self.vx, self.vy = 0, 0 --reset every frame + if love.keyboard.isDown(up) then + self.vx = cos(self.rotation) * (self.speed * dt) + self.vy = sin(self.rotation) * (self.speed * dt) + elseif love.keyboard.isDown(down) then + self.vx = cos(self.rotation) * (self.speed * dt) * -1 + self.vy = sin(self.rotation) * (self.speed * dt) * -1 + elseif love.keyboard.isDown(left) then + self.rotation = self.rotation - (self.rotSpeed * dt) + elseif love.keyboard.isDown(right) then + self.rotation = self.rotation + (self.rotSpeed * dt) + end + + self.collider:setLinearVelocity(self.vx, self.vy) +end + +function Player:updateCol() + if self.p == 1 then + self.x = self.collider:getX() + self.y = self.collider:getY() + elseif self.p == 2 then + self.x = self.collider:getX() + self.y = self.collider:getY() + end end -- Update method for the Player class function Player:update(dt) - if _G.GAMESTATE == "GAME" then - self.vx = 0 - self.vy = 0 - local bulletSpeed = 20000 - - if self.p == 1 then - -- Handle player 1 controls - if love.keyboard.isDown("w") then - self.vx = cos(self.rotation) * (self.speed * dt) - self.vy = sin(self.rotation) * (self.speed * dt) - elseif love.keyboard.isDown("s") then - self.vx = cos(self.rotation) * (self.speed * dt) * -1 - self.vy = sin(self.rotation) * (self.speed * dt) * -1 - elseif love.keyboard.isDown("a") then - self.rotation = self.rotation - (self.rotSpeed * dt) - elseif love.keyboard.isDown("d") then - self.rotation = self.rotation + (self.rotSpeed * dt) - end - - self.collider:setLinearVelocity(self.vx, self.vy) - - -- Check for collision with walls - if self.collider:enter("Wall") then - print("Player 1 collided with wall") - end - - if EnableKeyPress1 == true and GAMESTATE == "GAME" then - if love.keyboard.isDown("space") then - local newBullet = self:shoot(bulletSpeed) - table.insert(Bullets1, newBullet) - KeyPressTime1 = KeyDelay1 - EnableKeyPress1 = false - end - end - self.x = self.collider:getX() - self.y = self.collider:getY() - - elseif self.p == 2 then - -- Handle player 2 controls - if love.keyboard.isDown("up") then - self.vx = cos(self.rotation) * (self.speed * dt) - self.vy = sin(self.rotation) * (self.speed * dt) - elseif love.keyboard.isDown("down") then - self.vx = cos(self.rotation) * (self.speed * dt) * -1 - self.vy = sin(self.rotation) * (self.speed * dt) * -1 - elseif love.keyboard.isDown("left") then - self.rotation = self.rotation - (self.rotSpeed * dt) - elseif love.keyboard.isDown("right") then - self.rotation = self.rotation + (self.rotSpeed * dt) - end - self.collider:setLinearVelocity(self.vx, self.vy) - - -- Check for collision with walls - if self.collider:enter("Wall") then - print("Player 2 collided with wall") - end + local bulletSpeed = 20000 + + if EnableKeyPress1 == true and self.p == 1 then + if love.keyboard.isDown("space") then + local newBullet = self:shoot(bulletSpeed) + table.insert(Bullets1, newBullet) + KeyPressTime1 = KeyDelay1 + EnableKeyPress1 = false + end + end - if EnableKeyPress2 == true then - if love.keyboard.isDown("return") then - local newBullet = self:shoot(bulletSpeed) - table.insert(Bullets2, newBullet) - KeyPressTime2 = KeyDelay2 - EnableKeyPress2 = false - end - end + if EnableKeyPress2 == true and self.p == 2 then + if love.keyboard.isDown("return") then + local newBullet = self:shoot(bulletSpeed) + table.insert(Bullets2, newBullet) + KeyPressTime2 = KeyDelay2 + EnableKeyPress2 = false end - self.x = self.collider:getX() - self.y = self.collider:getY() end end