From 99b420cc7ff45b45df365f2830122a25e191040f Mon Sep 17 00:00:00 2001 From: Simon Kellet Date: Sun, 21 Jul 2024 18:07:07 +0100 Subject: [PATCH] format and stuff --- Game/GameKeyPressed.lua | 12 +- Game/UpdateGame.lua | 32 +- Menu/DrawMenu.lua | 61 +- Menu/MenuKeyPressed.lua | 7 +- Menu/UpdateMenu.lua | 3 +- Pause/DrawPause.lua | 65 +- Pause/PauseKeyPressed.lua | 44 +- Pause/UpdatePause.lua | 3 +- constants.lua | 14 +- game.love | Bin 9406659 -> 9405824 bytes libs/classic.lua | 69 +- libs/profile.lua | 244 ++-- libs/sti/atlas.lua | 317 +++-- libs/sti/graphics.lua | 2 +- libs/sti/init.lua | 632 +++++---- libs/sti/plugins/box2d.lua | 114 +- libs/sti/plugins/bump.lua | 103 +- libs/sti/utils.lua | 78 +- libs/windfield/init.lua | 2065 +++++++++++++++------------- libs/windfield/mlib/mlib.lua | 2469 ++++++++++++++++++---------------- main.lua | 48 +- mapsloader.lua | 4 +- player.lua | 108 +- 23 files changed, 3434 insertions(+), 3060 deletions(-) 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 3f38fce18deb75082093a64923e3d39cda1a4f92..4ced93bbaa008f48c5f77a419e08d86f8e053b1d 100644 GIT binary patch delta 44763 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±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 b)+08kItmDfCIBG-;eZVR@$?!9mjC|&?89LG delta 45214 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�ZO7IBRjwVWdow9&m%5tFf8k8#nPW=-rUAhO^Mg37QukZnU&58Qm- zI8$n-sbE+LRyf_iB3rZ;DG3tysz{|$FmXv`NvXFf!(IcJ;Y8A9#B@{0d}M^t6xkWx ziY-b>$afLA-KX?2J_;#~RC`s$Qcp*B8-OL%TXK`ug!i3kRO?=qx-{x@9ctS^cfN=tiI_a__`Qx;(&~o!28R;su_7rjD_-7}PtfcBJ^T*^FPa*uY5mLI4Z<+d* zn&~&M!z3Es*4)qnsCyjlAI+zfuksaRH}rP6s&85zLmS`@yYtl>FwL*Y#MDg1`_<5g(ioqYak;oaQ5H7&n2FY2 zd@&wt9s-B)v{^YX63TFjc(v=otUT1!@NJJDtBrc?yUE#&YD>zd@m@!ukMCPYm(}~) z6%1I%%|Z_&oUcag{S@{`z2kAX3+EVPgG5)A2}82`nU&1`1y}1)s?6z2Q7%)-fYK2) zr}57jds2E!S*r_)_(e+DHQX8%{O|4umo!g?&n9_)_WckGdJa3uz0n=&|8N;|XZiw-Pm=)trWqWfx(6~F@ocMLv& zfsk? z^6C^`C&wz3iegJ1Qe(j@|Czhd94XRQ(-$F2-a9>nJKdgl*6(7Fzc^biz;i8HUl7Dp_WV+CISW9x#i4fr&P+v2x_$=w6`3!dd=?mn?TYrf0e z9o;-ob$;VWIAv9^;{G${D!TaW{OE#|g)~m(0G=*ztmkbld-YpxbO$W&R|Z<~fTot| zcF(amO7ZUuXB6C5Xky!T=04F#PdG%Qp=j?MOl+>BL)s51%&&ZD4>|#Fk`}R&^#}?w zZc#-8ld!FhU?w!hT@R$W#W?9QRI#|p9jMYdb48Ap(n=(KYHn>Q_lTPyV=^`$S&jG5 z5|i==WMnasJ!W?RcZZHd{Y2ZuFKkKzVIB%57EaLg>*d4d32$OqnkL81qM@?c(_Zl< z-vjnhYz+H0qIeCd9L!axi(@PDW2?V}v{U=p1&kKn1QqPH64HbkLSBkc5&I>@e`D&X~A?9R_dkaY$?kg5Q<|oD`xU z2pa_=2?GTW4i*A>u>Cg$9w^Wo2uIGAb*Upa8GKCwPWCI{{y{bea&!YPQ1dnZiv(2W z1|)-g3mSC;%0mJ`xb8p=NGFi3J5UqyC#dIFYz>6#0aS-91KD^0UvU(cTN8B#B&{6#S zXn+Qa_XOe+9kc#o;OuJeWMXdd{%eZEE1qCBqM&U$I7xI8Y#G91aN68%V_q3)U9e&Ieitu*eg@0rd|KYFu#;W=@W- zZYGXyu779m^Do8GK*E6vy@7I2lH8yRZ=f2KH4jJvSDII-*sGvq4AT=n<7f1x{^zQ%vKjRCO zhZgnu9isw?_yMWN|9nLFzXc%+3i1PzgI^Wi@CA~Q{{f5-9<(t2$UOtJ?gu3J1(pAo z%y9j|8mk9!`2)EH{=`@RZ_1V4xWB+`o!>iI+Svc2E-(c9znVbzFNPtYb${S1^8dxd z{kI^7K&$~kLeQB%_|LCC0reAC4wQQV)?a!6kc#4e4<7%UL(U~=H2_EfDGfRY z=SunKp)m*uqCaZRbRP#&xr-x(0XP1?2q^y8jzjqGPDum{$^ZaKeie|M>sQi$_LF`s znf|*V6>z|RfPPKf>`i{RCeV>Tkmh$Le=fHEn{pQ!q!jp<6v#RMcVhYPq+Hm*e@`>P z)c{X3|0VhPKp+{^Ee_~95GW7DhYJ!50xCo8;DVBZfXozs&U1c26aSkvH6Ca^2;3~d zGoRnhg7Obf6>RDL?0K~WKu{1Kkm#4013R$)l;O=EP8qnr{b?N;h=35k{IdV=^uIp; z?sZ__?H@kSU_T9{ 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