diff --git a/src/conf.lua b/src/conf.lua index 0bd4901..1389b6e 100644 --- a/src/conf.lua +++ b/src/conf.lua @@ -5,5 +5,4 @@ function love.conf(t) t.window.width = 854 t.window.height = 480 - t.window.resizable = true end diff --git a/src/lib/breezefield/collider.lua b/src/lib/breezefield/collider.lua new file mode 100644 index 0000000..e0a6309 --- /dev/null +++ b/src/lib/breezefield/collider.lua @@ -0,0 +1,69 @@ +-- a Collider object, wrapping shape, body, and fixtue +local set_funcs, lp, lg, COLLIDER_TYPES = unpack( + require((...):gsub('collider', '') .. '/utils')) + +local Collider = {} +Collider.__index = Collider + + + +function Collider.new(world, collider_type, ...) + -- deprecated + return world:newCollider(collider_type, {...}) +end + +function Collider:draw_type() + if self.collider_type == 'Edge' or self.collider_type == 'Chain' then + return 'line' + end + return self.collider_type:lower() +end + +function Collider:__draw__() + self._draw_type = self._draw_type or self:draw_type() + local args + if self._draw_type == 'line' then + args = {self:getSpatialIdentity()} + else + args = {'line', self:getSpatialIdentity()} + end + love.graphics[self:draw_type()](unpack(args)) +end + +function Collider:draw() + self:__draw__() +end + + +function Collider:destroy() + self._world.colliders[self] = nil + self.fixture:setUserData(nil) + self.fixture:destroy() + self.body:destroy() +end + +function Collider:getSpatialIdentity() + if self.collider_type == 'Circle' then + return self:getX(), self:getY(), self:getRadius() + else + return self:getWorldPoints(self:getPoints()) + end +end + +function Collider:collider_contacts() + local contacts = self:getContacts() + local colliders = {} + for i, contact in ipairs(contacts) do + if contact:isTouching() then + local f1, f2 = contact:getFixtures() + if f1 == self.fixture then + colliders[#colliders+1] = f2:getUserData() + else + colliders[#colliders+1] = f1:getUserData() + end + end + end + return colliders +end + +return Collider diff --git a/src/lib/breezefield/init.lua b/src/lib/breezefield/init.lua new file mode 100644 index 0000000..a42f397 --- /dev/null +++ b/src/lib/breezefield/init.lua @@ -0,0 +1,24 @@ +-- breezefield: init.lua +--[[ + implements Collider and World objects + Collider wraps the basic functionality of shape, fixture, and body + World wraps world, and provides automatic drawing simplified collisions +]]-- + + + +local bf = {} + + +local Collider = require(... ..'/collider') +local World = require(... ..'/world') + + +function bf.newWorld(...) + return bf.World:new(...) +end + +bf.Collider = Collider +bf.World = World + +return bf diff --git a/src/lib/breezefield/utils.lua b/src/lib/breezefield/utils.lua new file mode 100644 index 0000000..910d06e --- /dev/null +++ b/src/lib/breezefield/utils.lua @@ -0,0 +1,32 @@ +-- function used for both +local function set_funcs(mainobject, subobject) + -- this function assigns functions of a subobject to a primary object + --[[ + mainobject: the table to which to assign the functions + subobject: the table whose functions to assign + no output + --]] + for k, v in pairs(subobject.__index) do + if k ~= '__gc' and k ~= '__eq' and k ~= '__index' + and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' + and k ~= 'typeOf'and k ~= 'getUserData' and k ~= 'setUserData' then + mainobject[k] = function(mainobject, ...) + return v(subobject, ...) + end + end + end +end + +local COLLIDER_TYPES = { + CIRCLE = "Circle", + CIRC = "Circle", + RECTANGLE = "Rectangle", + RECT = "Rectangle", + POLYGON = "Polygon", + POLY = "Polygon", + EDGE = 'Edge', + CHAIN = 'Chain' +} + + +return {set_funcs, love.physics, love.graphics, COLLIDER_TYPES} diff --git a/src/lib/breezefield/world.lua b/src/lib/breezefield/world.lua new file mode 100644 index 0000000..506121e --- /dev/null +++ b/src/lib/breezefield/world.lua @@ -0,0 +1,253 @@ +-- breezefield: World.lua +--[[ + World: has access to all the functions of love.physics.world + additionally stores all Collider objects assigned to it in + self.colliders (as key-value pairs) + can draw all its Colliders + by default, calls :collide on any colliders in it for postSolve + or for beginContact if the colliders are sensors +--]] +-- TODO make updating work from here too +-- TODO: update test and tutorial +local Collider = require((...):gsub('world', '') .. 'collider') +local set_funcs, lp, lg, COLLIDER_TYPES = unpack( + require((...):gsub('world', '') .. '/utils')) + + + +local World = {} +World.__index = World +function World:new(...) + -- create a new physics world + --[[ + inputs: (same as love.physics.newWorld) + xg: float, gravity in x direction + yg: float, gravity in y direction + sleep: boolean, whether bodies can sleep + outputs: + w: bf.World, the created world + ]]-- + + local w = {} + setmetatable(w, self) + w._world = lp.newWorld(...) + set_funcs(w, w._world) + w.update = nil -- to use our custom update + w.colliders = {} + + -- some functions defined here to use w without being passed it + + function w.collide(obja, objb, coll_type, ...) + -- collision event for two Colliders + local function run_coll(obj1, obj2, ...) + if obj1[coll_type] ~= nil then + local e = obj1[coll_type](obj1, obj2, ...) + if type(e) == 'function' then + w.collide_events[#w.collide_events+1] = e + end + end + end + + if obja ~= nil and objb ~= nil then + run_coll(obja, objb, ...) + run_coll(objb, obja, ...) + end + end + + function w.enter(a, b, ...) + return w.collision(a, b, 'enter', ...) + end + function w.exit(a, b, ...) + return w.collision(a, b, 'exit', ...) + end + function w.preSolve(a, b, ...) + return w.collision(a, b, 'preSolve', ...) + end + function w.postSolve(a, b, ...) + return w.collision(a, b, 'postSolve', ...) + end + + function w.collision(a, b, ...) + -- objects that hit one another can have collide methods + -- by default used as postSolve callback + local obja = a:getUserData(a) + local objb = b:getUserData(b) + w.collide(obja, objb, ...) + end + + w:setCallbacks(w.enter, w.exit, w.preSolve, w.postSolve) + w.collide_events = {} + return w +end + + +function World:draw(alpha, draw_over) + -- draw the world + --[[ + alpha: sets the alpha of the drawing, defaults to 1 + draw_over: draws the collision objects shapes even if their + .draw method is overwritten + --]] + + for _, c in pairs(self.colliders) do + c:draw(alpha) + if draw_over then + c:__draw__() + end + end +end + +function World:queryRectangleArea(x1, y1, x2, y2) + -- query a bounding-box aligned area for colliders + --[[ + inputs: + x1, y1, x2, y2: floats, the x and y coordinates of two points + outputs: + colls: table, all colliders in bounding box + --]] + + local colls = {} + local callback = function(fixture) + table.insert(colls, fixture:getUserData()) + return true + end + self:queryBoundingBox(x1, y1, x2, y2, callback) + return colls +end + +local function check_vertices(vertices) + if #vertices % 2 ~= 0 then + error('vertices must be a multiple of 2') + elseif #vertices < 4 then + error('must have at least 2 vertices with x and y each') + end +end + +local function query_region(world, coll_type, args) + local collider = world:newCollider(coll_type, args) + collider:setSensor(true) + world:_disable_callbacks() + world:update(0) + local colls = collider:collider_contacts(collider) + collider:destroy() + world:_enable_callbacks() + return colls +end + +function World:_disable_callbacks() + self._callbacks = {self._world:getCallbacks()} + self._world:setCallbacks() +end + +function World:_enable_callbacks() + self._world:setCallbacks(unpack(self._callbacks)) +end + +function World:queryPolygonArea(...) + -- query an area enclosed by the lines connecting a series of points + --[[ + inputs: + x1, y1, x2, y2, ... floats, the x and y positions defining polygon + outputs: + colls: table, all Colliders intersecting the area + --]] + local vertices = {...} + if type(vertices[1]) == 'table' then + vertices = vertices[1] + end + check_vertices(vertices) + return query_region(self, 'Polygon', vertices) +end + +function World:queryCircleArea(x, y, r) + -- get all colliders in a circle are + --[[ + inputs: + x, y, r: floats, x, y and radius of circle + outputs: + colls: table: colliders in area + ]]-- + return query_region(self, 'Circle', {x, y, r}) +end + +function World:queryEdgeArea(...) + -- get all colliders along a (series of) line(s) + --[[ + inputs: + x1, y1, x2, y2, ... floats, the x and y positions defining lines + outpts: + colls: table: colliders intersecting these lines + --]] + local vertices = {...} + if type(vertices[1]) == 'table' then + vertices = vertices[1] + end + check_vertices(vertices) + return query_region(self, 'Edge', vertices) +end + +function World:update(dt) + -- update physics world + self._world:update(dt) + for i, v in pairs(self.collide_events) do + v() + self.collide_events[i] = nil + end +end + +--[[ +create a new collider in this world + +args: + collider_type (string): the type of the collider (not case seinsitive). any of: + circle, rectangle, polygon, edge, chain. + shape_arguments (table): arguments required to instantiate shape. + circle: {x, y, radius} + rectangle: {x, y, width height} + polygon/edge/chain: {x1, y1, x2, y2, ...} + table_to_use (optional, table): table to generate as the collider +]]-- +function World:newCollider(collider_type, shape_arguments, table_to_use) + + local o = table_to_use or {} + setmetatable(o, Collider) + -- note that you will need to set static vs dynamic later + local _collider_type = COLLIDER_TYPES[collider_type:upper()] + assert(_collider_type ~= nil, "unknown collider type: "..collider_type) + collider_type = _collider_type + if collider_type == 'Circle' then + local x, y, r = unpack(shape_arguments) + o.body = lp.newBody(self._world, x, y, "dynamic") + o.shape = lp.newCircleShape(r) + elseif collider_type == "Rectangle" then + local x, y, w, h = unpack(shape_arguments) + o.body = lp.newBody(self._world, x, y, "dynamic") + o.shape = lp.newRectangleShape(w, h) + collider_type = "Polygon" + else + o.body = lp.newBody(self._world, 0, 0, "dynamic") + o.shape = lp['new'..collider_type..'Shape'](unpack(shape_arguments)) + end + + o.collider_type = collider_type + + o.fixture = lp.newFixture(o.body, o.shape, 1) + o.fixture:setUserData(o) + + set_funcs(o, o.body) + set_funcs(o, o.shape) + set_funcs(o, o.fixture) + + -- index by self for now + o._world = self + self.colliders[o] = o + return o +end + +function World:removeCollider(o) + o._world = nil + o.body:destroy() + self.colliders[o] = nil +end + +return World diff --git a/src/lib/bump.lua b/src/lib/bump.lua deleted file mode 100644 index af2509e..0000000 --- a/src/lib/bump.lua +++ /dev/null @@ -1,781 +0,0 @@ -local bump = { - _VERSION = 'bump v3.1.7', - _URL = 'https://github.com/kikito/bump.lua', - _DESCRIPTION = 'A collision detection library for Lua', - _LICENSE = [[ - MIT LICENSE - - Copyright (c) 2014 Enrique GarcĂ­a Cota - - 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. - ]] -} - ------------------------------------------- --- Auxiliary functions ------------------------------------------- -local DELTA = 1e-10 -- floating-point margin of error - -local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max - -local function sign(x) - if x > 0 then return 1 end - if x == 0 then return 0 end - return -1 -end - -local function nearest(x, a, b) - if abs(a - x) < abs(b - x) then return a else return b end -end - -local function assertType(desiredType, value, name) - if type(value) ~= desiredType then - error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')') - end -end - -local function assertIsPositiveNumber(value, name) - if type(value) ~= 'number' or value <= 0 then - error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')') - end -end - -local function assertIsRect(x,y,w,h) - assertType('number', x, 'x') - assertType('number', y, 'y') - assertIsPositiveNumber(w, 'w') - assertIsPositiveNumber(h, 'h') -end - -local defaultFilter = function() - return 'slide' -end - ------------------------------------------- --- Rectangle functions ------------------------------------------- - -local function rect_getNearestCorner(x,y,w,h, px, py) - return nearest(px, x, x+w), nearest(py, y, y+h) -end - --- This is a generalized implementation of the liang-barsky algorithm, which also returns --- the normals of the sides where the segment intersects. --- Returns nil if the segment never touches the rect --- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge -local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2) - ti1, ti2 = ti1 or 0, ti2 or 1 - local dx, dy = x2-x1, y2-y1 - local nx, ny - local nx1, ny1, nx2, ny2 = 0,0,0,0 - local p, q, r - - for side = 1,4 do - if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left - elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right - elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top - else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom - end - - if p == 0 then - if q <= 0 then return nil end - else - r = q / p - if p < 0 then - if r > ti2 then return nil - elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny - end - else -- p > 0 - if r < ti1 then return nil - elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny - end - end - end - end - - return ti1,ti2, nx1,ny1, nx2,ny2 -end - --- Calculates the minkowsky difference between 2 rects, which is another rect -local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) - return x2 - x1 - w1, - y2 - y1 - h1, - w1 + w2, - h1 + h2 -end - -local function rect_containsPoint(x,y,w,h, px,py) - return px - x > DELTA and py - y > DELTA and - x + w - px > DELTA and y + h - py > DELTA -end - -local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2) - return x1 < x2+w2 and x2 < x1+w1 and - y1 < y2+h2 and y2 < y1+h1 -end - -local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2) - local dx = x1 - x2 + (w1 - w2)/2 - local dy = y1 - y2 + (h1 - h2)/2 - return dx*dx + dy*dy -end - -local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY) - goalX = goalX or x1 - goalY = goalY or y1 - - local dx, dy = goalX - x1, goalY - y1 - local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) - - local overlaps, ti, nx, ny - - if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other - local px, py = rect_getNearestCorner(x,y,w,h, 0, 0) - local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection - ti = -wi * hi -- ti is the negative area of intersection - overlaps = true - else - local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge) - - -- item tunnels into other - if ti1 - and ti1 < 1 - and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner - and (0 < ti1 + DELTA - or 0 == ti1 and ti2 > 0) - then - ti, nx, ny = ti1, nx1, ny1 - overlaps = false - end - end - - if not ti then return end - - local tx, ty - - if overlaps then - if dx == 0 and dy == 0 then - -- intersecting and not moving - use minimum displacement vector - local px, py = rect_getNearestCorner(x,y,w,h, 0,0) - if abs(px) < abs(py) then py = 0 else px = 0 end - nx, ny = sign(px), sign(py) - tx, ty = x1 + px, y1 + py - else - -- intersecting and moving - move in the opposite direction - local ti1, _ - ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1) - if not ti1 then return end - tx, ty = x1 + dx * ti1, y1 + dy * ti1 - end - else -- tunnel - tx, ty = x1 + dx * ti, y1 + dy * ti - end - - return { - overlaps = overlaps, - ti = ti, - move = {x = dx, y = dy}, - normal = {x = nx, y = ny}, - touch = {x = tx, y = ty}, - itemRect = {x = x1, y = y1, w = w1, h = h1}, - otherRect = {x = x2, y = y2, w = w2, h = h2} - } -end - ------------------------------------------- --- Grid functions ------------------------------------------- - -local function grid_toWorld(cellSize, cx, cy) - return (cx - 1)*cellSize, (cy-1)*cellSize -end - -local function grid_toCell(cellSize, x, y) - return floor(x / cellSize) + 1, floor(y / cellSize) + 1 -end - --- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing", --- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf --- It has been modified to include both cells when the ray "touches a grid corner", --- and with a different exit condition - -local function grid_traverse_initStep(cellSize, ct, t1, t2) - local v = t2 - t1 - if v > 0 then - return 1, cellSize / v, ((ct + v) * cellSize - t1) / v - elseif v < 0 then - return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v - else - return 0, math.huge, math.huge - end -end - -local function grid_traverse(cellSize, x1,y1,x2,y2, f) - local cx1,cy1 = grid_toCell(cellSize, x1,y1) - local cx2,cy2 = grid_toCell(cellSize, x2,y2) - local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2) - local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2) - local cx,cy = cx1,cy1 - - f(cx, cy) - - -- The default implementation had an infinite loop problem when - -- approaching the last cell in some occassions. We finish iterating - -- when we are *next* to the last cell - while abs(cx - cx2) + abs(cy - cy2) > 1 do - if tx < ty then - tx, cx = tx + dx, cx + stepX - f(cx, cy) - else - -- Addition: include both cells when going through corners - if tx == ty then f(cx + stepX, cy) end - ty, cy = ty + dy, cy + stepY - f(cx, cy) - end - end - - -- If we have not arrived to the last cell, use it - if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end - -end - -local function grid_toCellRect(cellSize, x,y,w,h) - local cx,cy = grid_toCell(cellSize, x, y) - local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize) - return cx, cy, cr - cx + 1, cb - cy + 1 -end - ------------------------------------------- --- Responses ------------------------------------------- - -local touch = function(world, col, x,y,w,h, goalX, goalY, filter) - return col.touch.x, col.touch.y, {}, 0 -end - -local cross = function(world, col, x,y,w,h, goalX, goalY, filter) - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - -local slide = function(world, col, x,y,w,h, goalX, goalY, filter) - goalX = goalX or x - goalY = goalY or y - - local tch, move = col.touch, col.move - if move.x ~= 0 or move.y ~= 0 then - if col.normal.x ~= 0 then - goalX = tch.x - else - goalY = tch.y - end - end - - col.slide = {x = goalX, y = goalY} - - x,y = tch.x, tch.y - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - -local bounce = function(world, col, x,y,w,h, goalX, goalY, filter) - goalX = goalX or x - goalY = goalY or y - - local tch, move = col.touch, col.move - local tx, ty = tch.x, tch.y - - local bx, by = tx, ty - - if move.x ~= 0 or move.y ~= 0 then - local bnx, bny = goalX - tx, goalY - ty - if col.normal.x == 0 then bny = -bny else bnx = -bnx end - bx, by = tx + bnx, ty + bny - end - - col.bounce = {x = bx, y = by} - x,y = tch.x, tch.y - goalX, goalY = bx, by - - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - ------------------------------------------- --- World ------------------------------------------- - -local World = {} -local World_mt = {__index = World} - --- Private functions and methods - -local function sortByWeight(a,b) return a.weight < b.weight end - -local function sortByTiAndDistance(a,b) - if a.ti == b.ti then - local ir, ar, br = a.itemRect, a.otherRect, b.otherRect - local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h) - local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h) - return ad < bd - end - return a.ti < b.ti -end - -local function addItemToCell(self, item, cx, cy) - self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'}) - local row = self.rows[cy] - row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})} - local cell = row[cx] - self.nonEmptyCells[cell] = true - if not cell.items[item] then - cell.items[item] = true - cell.itemCount = cell.itemCount + 1 - end -end - -local function removeItemFromCell(self, item, cx, cy) - local row = self.rows[cy] - if not row or not row[cx] or not row[cx].items[item] then return false end - - local cell = row[cx] - cell.items[item] = nil - cell.itemCount = cell.itemCount - 1 - if cell.itemCount == 0 then - self.nonEmptyCells[cell] = nil - end - return true -end - -local function getDictItemsInCellRect(self, cl,ct,cw,ch) - local items_dict = {} - for cy=ct,ct+ch-1 do - local row = self.rows[cy] - if row then - for cx=cl,cl+cw-1 do - local cell = row[cx] - if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling - for item,_ in pairs(cell.items) do - items_dict[item] = true - end - end - end - end - end - - return items_dict -end - -local function getCellsTouchedBySegment(self, x1,y1,x2,y2) - - local cells, cellsLen, visited = {}, 0, {} - - grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy) - local row = self.rows[cy] - if not row then return end - local cell = row[cx] - if not cell or visited[cell] then return end - - visited[cell] = true - cellsLen = cellsLen + 1 - cells[cellsLen] = cell - end) - - return cells, cellsLen -end - -local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter) - local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2) - local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1 - local nx1,ny1 - local visited, itemInfo, itemInfoLen = {},{},0 - for i=1,len do - cell = cells[i] - for item in pairs(cell.items) do - if not visited[item] then - visited[item] = true - if (not filter or filter(item)) then - rect = self.rects[item] - l,t,w,h = rect.x,rect.y,rect.w,rect.h - - ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1) - if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then - -- the sorting is according to the t of an infinite line, not the segment - tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge) - itemInfoLen = itemInfoLen + 1 - itemInfo[itemInfoLen] = { - item = item, - ti1 = ti1, - ti2 = ti2, - nx = nx1, - ny = ny1, - weight = min(tii0,tii1) - } - end - end - end - end - end - table.sort(itemInfo, sortByWeight) - return itemInfo, itemInfoLen -end - -local function getResponseByName(self, name) - local response = self.responses[name] - if not response then - error(('Unknown collision type: %s (%s)'):format(name, type(name))) - end - return response -end - - --- Misc Public Methods - -function World:addResponse(name, response) - self.responses[name] = response -end - -function World:project(item, x,y,w,h, goalX, goalY, filter) - assertIsRect(x,y,w,h) - - goalX = goalX or x - goalY = goalY or y - filter = filter or defaultFilter - - local collisions, len = {}, 0 - - local visited = {} - if item ~= nil then visited[item] = true end - - -- This could probably be done with less cells using a polygon raster over the cells instead of a - -- bounding rect of the whole movement. Conditional to building a queryPolygon method - local tl, tt = min(goalX, x), min(goalY, y) - local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h) - local tw, th = tr-tl, tb-tt - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th) - - local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) - - for other,_ in pairs(dictItemsInCellRect) do - if not visited[other] then - visited[other] = true - - local responseName = filter(item, other) - if responseName then - local ox,oy,ow,oh = self:getRect(other) - local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY) - - if col then - col.other = other - col.item = item - col.type = responseName - - len = len + 1 - collisions[len] = col - end - end - end - end - - table.sort(collisions, sortByTiAndDistance) - - return collisions, len -end - -function World:countCells() - local count = 0 - for _,row in pairs(self.rows) do - for _,_ in pairs(row) do - count = count + 1 - end - end - return count -end - -function World:hasItem(item) - return not not self.rects[item] -end - -function World:getItems() - local items, len = {}, 0 - for item,_ in pairs(self.rects) do - len = len + 1 - items[len] = item - end - return items, len -end - -function World:countItems() - local len = 0 - for _ in pairs(self.rects) do len = len + 1 end - return len -end - -function World:getRect(item) - local rect = self.rects[item] - if not rect then - error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.') - end - return rect.x, rect.y, rect.w, rect.h -end - -function World:toWorld(cx, cy) - return grid_toWorld(self.cellSize, cx, cy) -end - -function World:toCell(x,y) - return grid_toCell(self.cellSize, x, y) -end - - ---- Query methods - -function World:queryRect(x,y,w,h, filter) - - assertIsRect(x,y,w,h) - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) - - local items, len = {}, 0 - - local rect - for item,_ in pairs(dictItemsInCellRect) do - rect = self.rects[item] - if (not filter or filter(item)) - and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h) - then - len = len + 1 - items[len] = item - end - end - - return items, len -end - -function World:queryPoint(x,y, filter) - local cx,cy = self:toCell(x,y) - local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1) - - local items, len = {}, 0 - - local rect - for item,_ in pairs(dictItemsInCellRect) do - rect = self.rects[item] - if (not filter or filter(item)) - and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y) - then - len = len + 1 - items[len] = item - end - end - - return items, len -end - -function World:querySegment(x1, y1, x2, y2, filter) - local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) - local items = {} - for i=1, len do - items[i] = itemInfo[i].item - end - return items, len -end - -function World:querySegmentWithCoords(x1, y1, x2, y2, filter) - local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) - local dx, dy = x2-x1, y2-y1 - local info, ti1, ti2 - for i=1, len do - info = itemInfo[i] - ti1 = info.ti1 - ti2 = info.ti2 - - info.weight = nil - info.x1 = x1 + dx * ti1 - info.y1 = y1 + dy * ti1 - info.x2 = x1 + dx * ti2 - info.y2 = y1 + dy * ti2 - end - return itemInfo, len -end - - ---- Main methods - -function World:add(item, x,y,w,h) - local rect = self.rects[item] - if rect then - error('Item ' .. tostring(item) .. ' added to the world twice.') - end - assertIsRect(x,y,w,h) - - self.rects[item] = {x=x,y=y,w=w,h=h} - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - for cy = ct, ct+ch-1 do - for cx = cl, cl+cw-1 do - addItemToCell(self, item, cx, cy) - end - end - - return item -end - -function World:remove(item) - local x,y,w,h = self:getRect(item) - - self.rects[item] = nil - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - for cy = ct, ct+ch-1 do - for cx = cl, cl+cw-1 do - removeItemFromCell(self, item, cx, cy) - end - end -end - -function World:update(item, x2,y2,w2,h2) - local x1,y1,w1,h1 = self:getRect(item) - w2,h2 = w2 or w1, h2 or h1 - assertIsRect(x2,y2,w2,h2) - - if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then - - local cellSize = self.cellSize - local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1) - local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2) - - if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then - - local cr1, cb1 = cl1+cw1-1, ct1+ch1-1 - local cr2, cb2 = cl2+cw2-1, ct2+ch2-1 - local cyOut - - for cy = ct1, cb1 do - cyOut = cy < ct2 or cy > cb2 - for cx = cl1, cr1 do - if cyOut or cx < cl2 or cx > cr2 then - removeItemFromCell(self, item, cx, cy) - end - end - end - - for cy = ct2, cb2 do - cyOut = cy < ct1 or cy > cb1 - for cx = cl2, cr2 do - if cyOut or cx < cl1 or cx > cr1 then - addItemToCell(self, item, cx, cy) - end - end - end - - end - - local rect = self.rects[item] - rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2 - - end -end - -function World:move(item, goalX, goalY, filter) - local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter) - - self:update(item, actualX, actualY) - - return actualX, actualY, cols, len -end - -function World:check(item, goalX, goalY, filter) - filter = filter or defaultFilter - - local visited = {[item] = true} - local visitedFilter = function(itm, other) - if visited[other] then return false end - return filter(itm, other) - end - - local cols, len = {}, 0 - - local x,y,w,h = self:getRect(item) - - local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter) - - while projected_len > 0 do - local col = projected_cols[1] - len = len + 1 - cols[len] = col - - visited[col.other] = true - - local response = getResponseByName(self, col.type) - - goalX, goalY, projected_cols, projected_len = response( - self, - col, - x, y, w, h, - goalX, goalY, - visitedFilter - ) - end - - return goalX, goalY, cols, len -end - - --- Public library functions - -bump.newWorld = function(cellSize) - cellSize = cellSize or 64 - assertIsPositiveNumber(cellSize, 'cellSize') - local world = setmetatable({ - cellSize = cellSize, - rects = {}, - rows = {}, - nonEmptyCells = {}, - responses = {} - }, World_mt) - - world:addResponse('touch', touch) - world:addResponse('cross', cross) - world:addResponse('slide', slide) - world:addResponse('bounce', bounce) - - return world -end - -bump.rect = { - getNearestCorner = rect_getNearestCorner, - getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices, - getDiff = rect_getDiff, - containsPoint = rect_containsPoint, - isIntersecting = rect_isIntersecting, - getSquareDistance = rect_getSquareDistance, - detectCollision = rect_detectCollision -} - -bump.responses = { - touch = touch, - cross = cross, - slide = slide, - bounce = bounce -} - -return bump diff --git a/src/main.lua b/src/main.lua index d29f588..3d32f3c 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,6 +1,8 @@ local Gamestate = require("lib.hump.gamestate") pprint = require("lib.pprint") +PLAYER_SIZE = 20 + function love.load() love.math.setRandomSeed(love.timer.getTime()) love.keyboard.setKeyRepeat(true) diff --git a/src/player.lua b/src/player.lua new file mode 100644 index 0000000..e399b7c --- /dev/null +++ b/src/player.lua @@ -0,0 +1,91 @@ +local Vec = require("lib.brinevector") + +local Player = {} +Player.__index = Player + +local SHOOT_COOLDOWN = 1 + +function Player:new(controls, collision_world, pos) + local player = setmetatable({}, Player) + player.pos = pos + player.vel = Vec() + player.controls = controls + player.collider = collision_world:newCollider("Circle", { pos.x, pos.y, PLAYER_SIZE }) + player.pickedup_bolt_speeds = {} + player.shoot_cooldown = nil + player.health = 3 + + if controls == "joystick" then + local joysticks = love.joystick.getJoysticks() + assert(joysticks[1], "no joystick connected") + player.joystick = joysticks[1] + end + return player +end + +local function boolToNum(bool) + return bool and 1 or 0 +end + +local function getDirectionKey(positive, negative) + return boolToNum(love.keyboard.isDown(positive)) - boolToNum(love.keyboard.isDown(negative)) +end + +function Player:get_move_dir() + if self.controls == "keyboard" then + local dirx = getDirectionKey("d", "a") + local diry = getDirectionKey("s", "w") + return Vec(dirx, diry).normalized + elseif self.controls == "joystick" then + local dirx = self.joystick:getGamepadAxis("leftx") + local diry = self.joystick:getGamepadAxis("lefty") + local size = dirx^2 + diry^2 + if size > 0.1 then + return Vec(dirx, diry).normalized + else + return Vec(0, 0) + end + else + return Vec(0, 0) + end +end + +function Player:get_aim_dir() + if self.controls == "keyboard" then + return (Vec(love.mouse.getPosition()) - self.pos).normalized + elseif self.controls == "joystick" then + local dirx = self.joystick:getGamepadAxis("rightx") + local diry = self.joystick:getGamepadAxis("righty") + local dir = Vec(dirx, diry).normalized + local size = dirx^2 + diry^2 + if size < 0.1 and self.last_aim_dir then + dir = self.last_aim_dir + end + self.last_aim_dir = dir + return dir + else + return Vec(0, 0) + end +end + +function Player:process_shoot() + if not self.on_shoot then return end + local now = love.timer.getTime() + if self.last_shot_time then + if now - self.last_shot_time < SHOOT_COOLDOWN then return end + end + + local shoot = false + if self.controls == "keyboard" then + shoot = love.keyboard.isDown("space") + elseif self.controls == "joystick" then + shoot = self.joystick:isGamepadDown("leftshoulder") + end + + if shoot then + self:on_shoot() + self.last_shot_time = now + end +end + +return Player diff --git a/src/states/main.lua b/src/states/main.lua index 05bac68..fb29cd1 100644 --- a/src/states/main.lua +++ b/src/states/main.lua @@ -1,29 +1,268 @@ local MainState = {} -local nata = require("lib.nata") +local Player = require("player") +local Vec = require("lib.brinevector") +local bf = require("lib.breezefield") + +local BOLT_DIST_FROM_PLAYER = 40 +local BOLT_SIZE = 8 + +local DRAW_COLLIDERS = false +local BOLT_DURATION = 2.5 + +local PLAYER1_MASK = 2 +local PLAYER2_MASK = 3 function MainState:enter() - self.ecs = nata.new{ - groups = {}, - systems = {}, - data = {} - } + love.mouse.setVisible(false) + + self.world = bf.newWorld(0, 0, true) + self.world:setCallbacks(function(...) self:on_begin_contact(...) end) + + local winw, winh = love.graphics.getDimensions() + self.player_1 = Player:new("keyboard", self.world, Vec(50, 200)) + self.player_2 = Player:new("joystick", self.world, Vec(winw-50, 200)) + + self.player_1.collider.fixture:setCategory(PLAYER1_MASK) + self.player_2.collider.fixture:setCategory(PLAYER2_MASK) + + self.players = { self.player_1, self.player_2 } + + local on_shoot = function(...) self:on_player_shoot(...) end + for _, player in ipairs(self.players) do + player.on_shoot = on_shoot + player.collider.fixture:setUserData("player") + end + + self.obstacles = {} + table.insert(self.obstacles, {{90, 90}, {100, 200}, {300, 300}, {200, 100}}) + table.insert(self.obstacles, {{390, 90}, {500, 200}, {500, 300}}) + table.insert(self.obstacles, {{450, 390}, {500, 200}, {500, 300}}) + table.insert(self.obstacles, {{750, 390}, {500, 200}, {500, 300}}) + table.insert(self.obstacles, {{750, 290}, {600, 200}, {600, 100}}) + table.insert(self.obstacles, {{550, 400}, {670, 500}, {500, 500}}) + table.insert(self.obstacles, {{700, 0}, {850, 200}, {850, 0}}) + table.insert(self.obstacles, {{250, 350}, {200, 500}, {300, 500}}) + + self.obstacle_colliders = {} + for _, obstacle in ipairs(self.obstacles) do + local points = {} + for _, p in ipairs(obstacle) do + table.insert(points, p[1]) + table.insert(points, p[2]) + end + local collider = self.world:newCollider("Polygon", points) + collider:setType("static") + table.insert(self.obstacle_colliders, collider) + end + + do + self.world:newCollider("Edge", { 0, 0, winw, 0 }):setType("static") -- top + self.world:newCollider("Edge", { 0, 0, 0, winh }):setType("static") -- left + self.world:newCollider("Edge", { winw, 0, winw, winh }):setType("static") -- right + self.world:newCollider("Edge", { 0, winh, winw, winh }):setType("static") -- bottom + end + + self.bolts = {} + self.bolt_colliders = {} end function MainState:update(dt) - self.ecs:flush() - self.ecs:emit("update", dt) + for _, player in ipairs(self.players) do + player:process_shoot() + + local move_dir = player:get_move_dir() + local acc = move_dir * 300 + player.vel = player.vel + acc * dt + player.vel = player.vel * 0.98 + player.collider:setLinearVelocity(player.vel.x, player.vel.y) + + local now = love.timer.getTime() + for i, bolt in ipairs(self.bolts) do + local collider = self.bolt_colliders[i] + local lifetime = now - bolt.created_at + local velx, vely = collider:getLinearVelocity() + if not bolt.dead then + bolt.max_vel = math.max(bolt.max_vel, (velx^2 + vely^2)^0.5) + end + if lifetime > BOLT_DURATION then + bolt.dead = true + local dampening = 0.9 + collider:setLinearVelocity(velx * dampening, vely * dampening) + end + + if bolt.dead then + local distance = (bolt.pos - player.pos).length + if distance < BOLT_DIST_FROM_PLAYER then + table.insert(player.pickedup_bolt_speeds, bolt.max_vel) + self:destroy_bolt(i) + end + end + end + end + + self.world:update(dt) + + for i, bolt in ipairs(self.bolts) do + local collider = self.bolt_colliders[i] + bolt.pos.x = collider:getX() + bolt.pos.y = collider:getY() + end + + for _, player in ipairs(self.players) do + player.pos.x = player.collider:getX() + player.pos.y = player.collider:getY() + end if love.keyboard.isDown("escape") then love.event.quit() end end -function MainState:keypressed(...) - self.ecs:emit("keypressed", ...) +function MainState:destroy_bolt(idx) + local collider = table.remove(self.bolt_colliders, idx) + table.remove(self.bolts, idx) + self.world:removeCollider(collider) +end + +function MainState:on_player_shoot(player) + local aim_dir = player:get_aim_dir() + local bolt = { + pos = player.pos + aim_dir * BOLT_DIST_FROM_PLAYER, + created_at = love.timer.getTime(), + max_vel = 0 + } + + local velocity = 500 + local pickup_idx = -1 + for i, vel in ipairs(player.pickedup_bolt_speeds) do + if vel > velocity then + velocity = vel + pickup_idx = i + end + end + if pickup_idx ~= -1 then + table.remove(player.pickedup_bolt_speeds, pickup_idx) + end + + local collider = self.world:newCollider("Circle", { bolt.pos.x, bolt.pos.y, BOLT_SIZE }) + collider:setLinearVelocity(aim_dir.x * velocity, aim_dir.y * velocity) + -- collider:applyLinearImpulse(aim_dir.x * launch_force, aim_dir.y * launch_force) + collider.fixture:setUserData("bolt") + collider:setRestitution(1.2) + + if player == self.player_1 then + collider:setMask(PLAYER1_MASK) + else + collider:setMask(PLAYER2_MASK) + end + + table.insert(self.bolts, bolt) + table.insert(self.bolt_colliders, collider) +end + +function MainState:get_bolt_by_collider(collider) + for i, other_collider in ipairs(self.bolt_colliders) do + if other_collider == collider then + return i + end + end +end + +function MainState:get_player_by_collider(collider) + if self.player_1.collider.fixture == collider then + return self.player_1 + elseif self.player_2.collider.fixture == collider then + return self.player_2 + end +end + +function MainState:on_player_touch_bolt(player, bolt_idx) + player.health = player.health - 1 + self:destroy_bolt(bolt_idx) +end + +function MainState:on_begin_contact(a, b) + local a_data = a:getUserData() + local b_data = b:getUserData() + if a_data == "player" and b_data == "bolt" then + self:on_player_touch_bolt(self:get_player_by_collider(a), self:get_bolt_by_collider(b)) + elseif a_data == "bolt" and b_data == "player" then + self:on_player_touch_bolt(self:get_bolt_by_collider(a), self:get_player_by_collider(b)) + end +end + +function MainState:draw_health(x, y, player, align) + local max_health = 3 + local rect_width = 20 + local rect_height = 25 + local gap = 10 + local padding = 10 + + local health_bar_width = max_health * (rect_width + gap) - gap + 2*padding + if align == "right" then + x = x - health_bar_width + end + + love.graphics.setColor(0.1, 0.1, 0.1) + love.graphics.rectangle("fill", + x, y, + health_bar_width, + rect_height + 2*padding + ) + + for i=1, max_health do + local rect_x = x + (rect_width + gap) * (i-1) + padding + if player.health >= i then + love.graphics.setColor(0.7, 0.2, 0.2) + else + love.graphics.setColor(0.2, 0.2, 0.2) + end + love.graphics.rectangle("fill", rect_x, y+padding, rect_width, rect_height) + end end function MainState:draw() - self.ecs:emit("draw") + for _, player in ipairs(self.players) do + love.graphics.setLineWidth(4) + love.graphics.setColor(1, 1, 1) + love.graphics.circle("line", player.pos.x, player.pos.y, PLAYER_SIZE) + + love.graphics.setLineWidth(3) + love.graphics.setColor(0.8, 0.1, 0.1) + local bolt_pos = player.pos + player:get_aim_dir() * BOLT_DIST_FROM_PLAYER + love.graphics.circle("line", bolt_pos.x, bolt_pos.y, BOLT_SIZE) + end + + for _, bolt in ipairs(self.bolts) do + love.graphics.circle("line", bolt.pos.x, bolt.pos.y, BOLT_SIZE) + end + + love.graphics.setLineWidth(3) + love.graphics.setColor(0.33, 0.33, 0.33) + for _, obstacle in ipairs(self.obstacles) do + local n = #obstacle + love.graphics.line(obstacle[1][1], obstacle[1][2], obstacle[n][1], obstacle[n][2]) + for i=1, n-1 do + love.graphics.line(obstacle[i][1], obstacle[i][2], obstacle[i+1][1], obstacle[i+1][2]) + end + end + + local cursor_size = 10 + local mx, my = love.mouse.getPosition() + love.graphics.setColor(1, 1, 1) + love.graphics.line(mx-cursor_size, my, mx+cursor_size, my) + love.graphics.line(mx, my-cursor_size, mx, my+cursor_size) + -- love.graphics.print(mx, 200, 10) + -- love.graphics.print(my, 200, 40) + + if DRAW_COLLIDERS then + love.graphics.setColor(0.8, 0.1, 0.8) + self.world:draw() + end + + local winw, winh = love.graphics.getDimensions() + self:draw_health(10, 10, self.player_1, "left") + self:draw_health(winw-10, 10, self.player_2, "right") end return MainState diff --git a/src/ui/button.lua b/src/ui/button.lua deleted file mode 100644 index ed6f9ce..0000000 --- a/src/ui/button.lua +++ /dev/null @@ -1,23 +0,0 @@ -return function(ui, text, x, y, w, h) - local mx, my = love.mouse.getPosition() - local bg = ui.theme.bg_color - local result = false - if ui.inRect(mx, my, x, y, w, h) then - bg = ui.theme.bg_hover_color - if love.mouse.isDown(1) then - bg = ui.theme.bg_pressed_color - elseif ui:wasDown(1) then - result = true - end - end - - love.graphics.setColor(bg) - love.graphics.rectangle("fill", x, y, w, h) - - local font_height = ui.theme.font:getHeight() - love.graphics.setFont(ui.theme.font) - love.graphics.setColor(ui.theme.text_color) - love.graphics.printf(text, x, y+(h-font_height)/2, w, "center") - - return result -end diff --git a/src/ui/init.lua b/src/ui/init.lua deleted file mode 100644 index 3450329..0000000 --- a/src/ui/init.lua +++ /dev/null @@ -1,51 +0,0 @@ -local UI = {} -UI.__index = UI - -local elements = { - label = require(... .. ".label"), - button = require(... .. ".button"), - textbox = require(... .. ".textbox"), -} - -function UI.new(theme) - local self = setmetatable({}, UI) - self.theme = theme or {} - self.mouse_buttons = {} - return self -end - -function UI:postDraw() - self.keys_pressed = {} - self.entered_text = nil - self.mouse_buttons[1] = love.mouse.isDown(1) - self.mouse_buttons[2] = love.mouse.isDown(2) - self.mouse_buttons[3] = love.mouse.isDown(3) -end - -function UI:keypressed(key) - self.keys_pressed[key] = true -end - -function UI:textinput(text) - self.entered_text = text -end - -function UI.inRect(mx, my, x, y, w, h) - return (x <= mx and mx < x+w) and (y <= my and my < y+h) -end - -function UI:wasDown(...) - for i=1, select("#", ...) do - local button = select(i, ...) - if self.mouse_buttons[button] then - return true - end - end - return false -end - -function UI.__index(t, k) - return UI[k] or elements[k] -end - -return UI diff --git a/src/ui/label.lua b/src/ui/label.lua deleted file mode 100644 index 7f923a6..0000000 --- a/src/ui/label.lua +++ /dev/null @@ -1,5 +0,0 @@ -return function (ui, text, x, y) - love.graphics.setFont(ui.theme.font) - love.graphics.setColor(ui.theme.text_color) - love.graphics.print(text, x, y) -end diff --git a/src/ui/textbox.lua b/src/ui/textbox.lua deleted file mode 100644 index d473d84..0000000 --- a/src/ui/textbox.lua +++ /dev/null @@ -1,54 +0,0 @@ -local padding = 10 - -return function(ui, textbox, x, y, w, h) - local mx, my = love.mouse.getPosition() - local bg = ui.theme.bg_color - if ui.inRect(mx, my, x, y, w, h) then - bg = ui.theme.bg_hover_color - if love.mouse.isDown(1) then - bg = ui.theme.bg_pressed_color - elseif ui:wasDown(1) then - textbox.is_editing = true - textbox.cursor = #textbox.text - end - else - if love.mouse.isDown(1) then - textbox.is_editing = false - end - end - - love.graphics.setColor(bg) - love.graphics.rectangle("fill", x, y, w, h) - - love.graphics.setColor(ui.theme.text_color) - if textbox.is_editing then - if ui.keys_pressed["escape"] then - textbox.is_editing = false - elseif ui.keys_pressed["left"] then - textbox.cursor = math.max(textbox.cursor - 1, 0) - elseif ui.keys_pressed["right"] then - textbox.cursor = math.min(textbox.cursor + 1, #textbox.text) - elseif ui.keys_pressed["backspace"] and textbox.cursor > 0 then - textbox.text = textbox.text:sub(0, textbox.cursor-1) .. textbox.text:sub(textbox.cursor+1) - textbox.cursor = textbox.cursor - 1 - elseif ui.keys_pressed["v"] and love.keyboard.isDown("lctrl", "rctrl") then - local text = love.system.getClipboardText() - textbox.text = textbox.text:sub(1, textbox.cursor) .. text .. textbox.text:sub(textbox.cursor+1) - textbox.cursor = textbox.cursor + #text - end - - if ui.entered_text then - textbox.text = textbox.text:sub(1, textbox.cursor) .. ui.entered_text .. textbox.text:sub(textbox.cursor+1) - textbox.cursor = textbox.cursor + #ui.entered_text - end - - love.graphics.setLineWidth(3) - love.graphics.line(x, y+h-1.5, x+w, y+h-1.5) - local cursor_ox = ui.theme.font:getWidth(textbox.text:sub(1, textbox.cursor))+padding - love.graphics.line(x+cursor_ox, y+padding, x+cursor_ox, y+h-padding) - end - - local font_height = ui.theme.font:getHeight() - love.graphics.setFont(ui.theme.font) - love.graphics.print(textbox.text or "", x+padding, y+(h-font_height)/2) -end