version 0.1
This commit is contained in:
parent
cc709ba84a
commit
e2134d76bd
@ -5,5 +5,4 @@ function love.conf(t)
|
||||
|
||||
t.window.width = 854
|
||||
t.window.height = 480
|
||||
t.window.resizable = true
|
||||
end
|
||||
|
69
src/lib/breezefield/collider.lua
Normal file
69
src/lib/breezefield/collider.lua
Normal file
@ -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
|
24
src/lib/breezefield/init.lua
Normal file
24
src/lib/breezefield/init.lua
Normal file
@ -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
|
32
src/lib/breezefield/utils.lua
Normal file
32
src/lib/breezefield/utils.lua
Normal file
@ -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}
|
253
src/lib/breezefield/world.lua
Normal file
253
src/lib/breezefield/world.lua
Normal file
@ -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
|
781
src/lib/bump.lua
781
src/lib/bump.lua
@ -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
|
@ -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)
|
||||
|
91
src/player.lua
Normal file
91
src/player.lua
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user