First demo commit.

This commit is contained in:
bakpakin
2015-04-27 14:12:11 +08:00
commit a63329422d
57 changed files with 5201 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
local assert, pairs, type, tostring, setmetatable = assert, pairs, type, tostring, setmetatable
local baseMt, _instances, _classes, _class = {}, setmetatable({},{__mode='k'}), setmetatable({},{__mode='k'})
local function assert_class(class, method) assert(_classes[class], ('Wrong method call. Expected class:%s.'):format(method)) end
local function deep_copy(t, dest, aType) t = t or {}; local r = dest or {}
for k,v in pairs(t) do
if aType and type(v)==aType then r[k] = v elseif not aType then
if type(v) == 'table' and k ~= "__index" then r[k] = deep_copy(v) else r[k] = v end
end
end; return r
end
local function instantiate(self,...)
assert_class(self, 'new(...) or class(...)'); local instance = {class = self}; _instances[instance] = tostring(instance); setmetatable(instance,self)
if self.init then if type(self.init) == 'table' then deep_copy(self.init, instance) else self.init(instance, ...) end; end; return instance
end
local function extend(self, name, extra_params)
assert_class(self, 'extend(...)'); local heir = {}; _classes[heir] = tostring(heir); deep_copy(extra_params, deep_copy(self, heir));
heir.name, heir.__index, heir.super = extra_params and extra_params.name or name, heir, self; return setmetatable(heir,self)
end
baseMt = { __call = function (self,...) return self:new(...) end, __tostring = function(self,...)
if _instances[self] then return ("instance of '%s' (%s)"):format(rawget(self.class,'name') or '?', _instances[self]) end
return _classes[self] and ("class '%s' (%s)"):format(rawget(self,'name') or '?',_classes[self]) or self
end}; _classes[baseMt] = tostring(baseMt); setmetatable(baseMt, {__tostring = baseMt.__tostring})
local class = {isClass = function(class, ofsuper) local isclass = not not _classes[class]; if ofsuper then return isclass and (class.super == ofsuper) end; return isclass end, isInstance = function(instance, ofclass)
local isinstance = not not _instances[instance]; if ofclass then return isinstance and (instance.class == ofclass) end; return isinstance end}; _class = function(name, attr)
local c = deep_copy(attr); c.mixins=setmetatable({},{__mode='k'}); _classes[c] = tostring(c); c.name, c.__tostring, c.__call = name or c.name, baseMt.__tostring, baseMt.__call
c.include = function(self,mixin) assert_class(self, 'include(mixin)'); self.mixins[mixin] = true; return deep_copy(mixin, self, 'function') end
c.new, c.extend, c.__index, c.includes = instantiate, extend, c, function(self,mixin) assert_class(self,'includes(mixin)') return not not (self.mixins[mixin] or (self.super and self.super:includes(mixin))) end
c.extends = function(self, class) assert_class(self, 'extends(class)') local super = self; repeat super = super.super until (super == class or super == nil); return class and (super == class) end
return setmetatable(c, baseMt) end; class._DESCRIPTION = '30 lines library for object orientation in Lua'; class._VERSION = '30log v1.0.0'; class._URL = 'http://github.com/Yonaba/30log'; class._LICENSE = 'MIT LICENSE <http://www.opensource.org/licenses/mit-license.php>'
return setmetatable(class,{__call = function(_,...) return _class(...) end })
+284
View File
@@ -0,0 +1,284 @@
local anim8 = {
_VERSION = 'anim8 v2.1.0',
_DESCRIPTION = 'An animation library for LÖVE',
_URL = 'https://github.com/kikito/anim8',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2011 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.
]]
}
local Grid = {}
local _frames = {}
local function assertPositiveInteger(value, name)
if type(value) ~= 'number' then error(("%s should be a number, was %q"):format(name, tostring(value))) end
if value < 1 then error(("%s should be a positive number, was %d"):format(name, value)) end
if value ~= math.floor(value) then error(("%s should be an integer, was %d"):format(name, value)) end
end
local function createFrame(self, x, y)
local fw, fh = self.frameWidth, self.frameHeight
return love.graphics.newQuad(
self.left + (x-1) * fw + x * self.border,
self.top + (y-1) * fh + y * self.border,
fw,
fh,
self.imageWidth,
self.imageHeight
)
end
local function getGridKey(...)
return table.concat( {...} ,'-' )
end
local function getOrCreateFrame(self, x, y)
if x < 1 or x > self.width or y < 1 or y > self.height then
error(("There is no frame for x=%d, y=%d"):format(x, y))
end
local key = self._key
_frames[key] = _frames[key] or {}
_frames[key][x] = _frames[key][x] or {}
_frames[key][x][y] = _frames[key][x][y] or createFrame(self, x, y)
return _frames[key][x][y]
end
local function parseInterval(str)
if type(str) == "number" then return str,str,1 end
str = str:gsub('%s', '') -- remove spaces
local min, max = str:match("^(%d+)-(%d+)$")
assert(min and max, ("Could not parse interval from %q"):format(str))
min, max = tonumber(min), tonumber(max)
local step = min <= max and 1 or -1
return min, max, step
end
function Grid:getFrames(...)
local result, args = {}, {...}
local minx, maxx, stepx, miny, maxy, stepy
for i=1, #args, 2 do
minx, maxx, stepx = parseInterval(args[i])
miny, maxy, stepy = parseInterval(args[i+1])
for y = miny, maxy, stepy do
for x = minx, maxx, stepx do
result[#result+1] = getOrCreateFrame(self,x,y)
end
end
end
return result
end
local Gridmt = {
__index = Grid,
__call = Grid.getFrames
}
local function newGrid(frameWidth, frameHeight, imageWidth, imageHeight, left, top, border)
assertPositiveInteger(frameWidth, "frameWidth")
assertPositiveInteger(frameHeight, "frameHeight")
assertPositiveInteger(imageWidth, "imageWidth")
assertPositiveInteger(imageHeight, "imageHeight")
left = left or 0
top = top or 0
border = border or 0
local key = getGridKey(frameWidth, frameHeight, imageWidth, imageHeight, left, top, border)
local grid = setmetatable(
{ frameWidth = frameWidth,
frameHeight = frameHeight,
imageWidth = imageWidth,
imageHeight = imageHeight,
left = left,
top = top,
border = border,
width = math.floor(imageWidth/frameWidth),
height = math.floor(imageHeight/frameHeight),
_key = key
},
Gridmt
)
return grid
end
-----------------------------------------------------------
local Animation = {}
local function cloneArray(arr)
local result = {}
for i=1,#arr do result[i] = arr[i] end
return result
end
local function parseDurations(durations, frameCount)
local result = {}
if type(durations) == 'number' then
for i=1,frameCount do result[i] = durations end
else
local min, max, step
for key,duration in pairs(durations) do
assert(type(duration) == 'number', "The value [" .. tostring(duration) .. "] should be a number")
min, max, step = parseInterval(key)
for i = min,max,step do result[i] = duration end
end
end
if #result < frameCount then
error("The durations table has length of " .. tostring(#result) .. ", but it should be >= " .. tostring(frameCount))
end
return result
end
local function parseIntervals(durations)
local result, time = {0},0
for i=1,#durations do
time = time + durations[i]
result[i+1] = time
end
return result, time
end
local Animationmt = { __index = Animation }
local nop = function() end
local function newAnimation(frames, durations, onLoop)
local td = type(durations);
if (td ~= 'number' or durations <= 0) and td ~= 'table' then
error("durations must be a positive number. Was " .. tostring(durations) )
end
onLoop = onLoop or nop
durations = parseDurations(durations, #frames)
local intervals, totalDuration = parseIntervals(durations)
return setmetatable({
frames = cloneArray(frames),
durations = durations,
intervals = intervals,
totalDuration = totalDuration,
onLoop = onLoop,
timer = 0,
position = 1,
status = "playing",
flippedH = false,
flippedV = false
},
Animationmt
)
end
function Animation:clone()
local newAnim = newAnimation(self.frames, self.durations, self.onLoop)
newAnim.flippedH, newAnim.flippedV = self.flippedH, self.flippedV
return newAnim
end
function Animation:flipH()
self.flippedH = not self.flippedH
return self
end
function Animation:flipV()
self.flippedV = not self.flippedV
return self
end
local function seekFrameIndex(intervals, timer)
local high, low, i = #intervals-1, 1, 1
while(low <= high) do
i = math.floor((low + high) / 2)
if timer > intervals[i+1] then low = i + 1
elseif timer <= intervals[i] then high = i - 1
else
return i
end
end
return i
end
function Animation:update(dt)
if self.status ~= "playing" then return end
self.timer = self.timer + dt
local loops = math.floor(self.timer / self.totalDuration)
if loops ~= 0 then
self.timer = self.timer - self.totalDuration * loops
local f = type(self.onLoop) == 'function' and self.onLoop or self[self.onLoop]
f(self, loops)
end
self.position = seekFrameIndex(self.intervals, self.timer)
end
function Animation:pause()
self.status = "paused"
end
function Animation:gotoFrame(position)
self.position = position
self.timer = self.intervals[self.position]
end
function Animation:pauseAtEnd()
self.position = #self.frames
self.timer = self.totalDuration
self:pause()
end
function Animation:pauseAtStart()
self.position = 1
self.timer = 0
self:pause()
end
function Animation:resume()
self.status = "playing"
end
function Animation:draw(image, x, y, r, sx, sy, ox, oy, ...)
local frame = self.frames[self.position]
if self.flippedH or self.flippedV then
r,sx,sy,ox,oy = r or 0, sx or 1, sy or 1, ox or 0, oy or 0
local _,_,w,h = frame:getViewport()
if self.flippedH then
sx = sx * -1
ox = w - ox
end
if self.flippedV then
sy = sy * -1
oy = h - oy
end
end
love.graphics.draw(image, frame, x, y, r, sx, sy, ox, oy, ...)
end
-----------------------------------------------------------
anim8.newGrid = newGrid
anim8.newAnimation = newAnimation
return anim8
+174
View File
@@ -0,0 +1,174 @@
-- beholder.lua - v2.1.1 (2011-11)
-- Copyright (c) 2011 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 callback OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function copy(t)
local c={}
for i=1,#t do c[i]=t[i] end
return c
end
local function hash2array(t)
local arr, i = {}, 0
for _,v in pairs(t) do
i = i+1
arr[i] = v
end
return arr, i
end
-- private Node class
local nodesById = nil
local root = nil
local function newNode()
return { callbacks = {}, children = setmetatable({}, {__mode="k"}) }
end
local function findNodeById(id)
return nodesById[id]
end
local function findOrCreateChildNode(self, key)
self.children[key] = self.children[key] or newNode()
return self.children[key]
end
local function findOrCreateDescendantNode(self, keys)
local node = self
for i=1, #keys do
node = findOrCreateChildNode(node, keys[i])
end
return node
end
local function invokeNodeCallbacks(self, params)
-- copy the hash into an array, for safety (self-erasures)
local callbacks, count = hash2array(self.callbacks)
for i=1,#callbacks do
callbacks[i](unpack(params))
end
return count
end
local function invokeAllNodeCallbacksInSubTree(self, params)
local counter = invokeNodeCallbacks(self, params)
for _,child in pairs(self.children) do
counter = counter + invokeAllNodeCallbacksInSubTree(child, params)
end
return counter
end
local function invokeNodeCallbacksFromPath(self, path)
local node = self
local params = copy(path)
local counter = invokeNodeCallbacks(node, params)
for i=1, #path do
node = node.children[path[i]]
if not node then break end
table.remove(params, 1)
counter = counter + invokeNodeCallbacks(node, params)
end
return counter
end
local function addCallbackToNode(self, callback)
local id = {}
self.callbacks[id] = callback
nodesById[id] = self
return id
end
local function removeCallbackFromNode(self, id)
self.callbacks[id] = nil
nodesById[id] = nil
end
------ beholder table
local beholder = {}
-- beholder private functions/vars
local groups = nil
local currentGroupId = nil
local function addIdToCurrentGroup(id)
if currentGroupId then
groups[currentGroupId] = groups[currentGroupId] or setmetatable({}, {__mode="k"})
local group = groups[currentGroupId]
group[#group + 1] = id
end
return id
end
local function stopObservingGroup(group)
local count = #group
for i=1,count do
beholder.stopObserving(group[i])
end
return count
end
local function falseIfZero(n)
return n > 0 and n
end
local function extractEventAndCallbackFromParams(params)
assert(#params > 0, "beholder.observe requires at least one parameter - the callback. You usually want to use two, i.e.: beholder.observe('EVENT', callback)")
local callback = table.remove(params, #params)
return params, callback
end
------ Public interface
function beholder.observe(...)
local event, callback = extractEventAndCallbackFromParams({...})
local node = findOrCreateDescendantNode(root, event)
return addIdToCurrentGroup(addCallbackToNode(node, callback))
end
function beholder.stopObserving(id)
local node = findNodeById(id)
if node then removeCallbackFromNode(node, id) end
local group, count = groups[id], 0
if group then count = stopObservingGroup(group) end
return (node or count > 0) and true or false
end
function beholder.group(groupId, f)
assert(not currentGroupId, "beholder.group can not be nested!")
currentGroupId = groupId
f()
currentGroupId = nil
end
function beholder.trigger(...)
return falseIfZero( invokeNodeCallbacksFromPath(root, {...}) )
end
function beholder.triggerAll(...)
return falseIfZero( invokeAllNodeCallbacksInSubTree(root, {...}) )
end
function beholder.reset()
root = newNode()
nodesById = setmetatable({}, {__mode="k"})
groups = {}
currentGroupId = nil
end
beholder.reset()
return beholder
+770
View File
@@ -0,0 +1,770 @@
local bump = {
_VERSION = 'bump v3.1.2',
_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 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 delta = 0.00001 -- floating-point-safe comparisons here, otherwise bugs
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 (0 < ti1 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)
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)
local touch = col.touch
return touch.x, touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local touch = col.touch
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 touch, move = col.touch, col.move
local sx, sy = touch.x, touch.y
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x == 0 then
sx = goalX
else
sy = goalY
end
end
col.slide = {x = sx, y = sy}
x,y = touch.x, touch.y
goalX, goalY = sx, sy
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 touch, move = col.touch, col.move
local tx, ty = touch.x, touch.y
local bx, by, bnx, bny = tx, ty, 0,0
if move.x ~= 0 or move.y ~= 0 then
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 = touch.x, touch.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
local onewayplatform = function(world, col, x,y,w,h, goalX, goalY, filter)
if col.normal.y < 0 and not col.overlaps then
col.didTouch = true
goalX, goalY, cols, len = slide(world, col, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
else
goalX, goalY, cols, len = cross(world, col, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
end
local onewayplatformTouch = function(world, col, x,y,w,h, goalX, goalY, filter)
if col.normal.y < 0 and not col.overlaps then
col.didTouch = true
goalX, goalY, cols, len = touch(world, col, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
else
goalX, goalY, cols, len = cross(world, col, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
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 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 = 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, 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)
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 x,y,w,h = self:getRect(item)
w2,h2 = w2 or w, h2 or h
assertIsRect(x2,y2,w2,h2)
if x ~= x2 or y ~= y2 or w ~= w2 or h ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x,y,w,h)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1==cl2 and ct1==ct2 and cw1==cw2 and ch1==ch2 then
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
else
self:remove(item)
self:add(item, x2,y2,w2,h2)
end
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(item, other)
if visited[other] then return false end
return filter(item, 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)
-- modified here
world:addResponse('onewayplatform', onewayplatform)
world:addResponse('onewayplatformTouch', onewayplatformTouch)
-- to here
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
+210
View File
@@ -0,0 +1,210 @@
-- gamera.lua v1.0.1
-- Copyright (c) 2012 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.
-- Based on YaciCode, from Julien Patte and LuaObject, from Sebastien Rocca-Serra
local gamera = {}
-- Private attributes and methods
local gameraMt = {__index = gamera}
local abs, min, max = math.abs, math.min, math.max
local function clamp(x, minX, maxX)
return x < minX and minX or (x>maxX and maxX or x)
end
local function checkNumber(value, name)
if type(value) ~= 'number' then
error(name .. " must be a number (was: " .. tostring(value) .. ")")
end
end
local function checkPositiveNumber(value, name)
if type(value) ~= 'number' or value <=0 then
error(name .. " must be a positive number (was: " .. tostring(value) ..")")
end
end
local function checkAABB(l,t,w,h)
checkNumber(l, "l")
checkNumber(t, "t")
checkPositiveNumber(w, "w")
checkPositiveNumber(h, "h")
end
local function getVisibleArea(self, scale)
scale = scale or self.scale
local sin, cos = abs(self.sin), abs(self.cos)
local w,h = self.w / scale, self.h / scale
w,h = cos*w + sin*h, sin*w + cos*h
return min(w,self.ww), min(h, self.wh)
end
local function cornerTransform(self, x,y)
local scale, sin, cos = self.scale, self.sin, self.cos
x,y = x - self.x, y - self.y
x,y = -cos*x + sin*y, -sin*x - cos*y
return self.x - (x/scale + self.l), self.y - (y/scale + self.t)
end
local function adjustPosition(self)
local wl,wt,ww,wh = self.wl, self.wt, self.ww, self.wh
local w,h = getVisibleArea(self)
local w2,h2 = w*0.5, h*0.5
local left, right = wl + w2, wl + ww - w2
local top, bottom = wt + h2, wt + wh - h2
self.x, self.y = clamp(self.x, left, right), clamp(self.y, top, bottom)
end
local function adjustScale(self)
local w,h,ww,wh = self.w, self.h, self.ww, self.wh
local rw,rh = getVisibleArea(self, 1) -- rotated frame: area around the window, rotated without scaling
local sx,sy = rw/ww, rh/wh -- vert/horiz scale: minimun scales that the window needs to occupy the world
local rscale = max(sx,sy)
self.scale = max(self.scale, rscale)
end
-- Public interface
function gamera.new(l,t,w,h)
local sw,sh = love.graphics.getWidth(), love.graphics.getHeight()
local cam = setmetatable({
x=0, y=0,
scale=1,
angle=0, sin=math.sin(0), cos=math.cos(0),
l=0, t=0, w=sw, h=sh, w2=sw*0.5, h2=sh*0.5
}, gameraMt)
cam:setWorld(l,t,w,h)
return cam
end
function gamera:setWorld(l,t,w,h)
checkAABB(l,t,w,h)
self.wl, self.wt, self.ww, self.wh = l,t,w,h
adjustPosition(self)
end
function gamera:setWindow(l,t,w,h)
checkAABB(l,t,w,h)
self.l, self.t, self.w, self.h, self.w2, self.h2 = l,t,w,h, w*0.5, h*0.5
adjustPosition(self)
end
function gamera:setPosition(x,y)
checkNumber(x, "x")
checkNumber(y, "y")
self.x, self.y = x,y
adjustPosition(self)
end
function gamera:setScale(scale)
checkNumber(scale, "scale")
self.scale = scale
adjustScale(self)
adjustPosition(self)
end
function gamera:setAngle(angle)
checkNumber(angle, "angle")
self.angle = angle
self.cos, self.sin = math.cos(angle), math.sin(angle)
adjustScale(self)
adjustPosition(self)
end
function gamera:getWorld()
return self.wl, self.wt, self.ww, self.wh
end
function gamera:getWindow()
return self.l, self.t, self.w, self.h
end
function gamera:getPosition()
return self.x, self.y
end
function gamera:getScale()
return self.scale
end
function gamera:getAngle()
return self.angle
end
function gamera:getVisible()
local w,h = getVisibleArea(self)
return self.x - w*0.5, self.y - h*0.5, w, h
end
function gamera:getVisibleCorners()
local x,y,w2,h2 = self.x, self.y, self.w2, self.h2
local x1,y1 = cornerTransform(self, x-w2,y-h2)
local x2,y2 = cornerTransform(self, x+w2,y-h2)
local x3,y3 = cornerTransform(self, x+w2,y+h2)
local x4,y4 = cornerTransform(self, x-w2,y+h2)
return x1,y1,x2,y2,x3,y3,x4,y4
end
-- modified here
function gamera:apply()
love.graphics.setScissor(self:getWindow())
love.graphics.push()
local scale = self.scale
love.graphics.scale(scale)
love.graphics.translate((self.w2 + self.l) / scale, (self.h2+self.t) / scale)
love.graphics.rotate(-self.angle)
love.graphics.translate(-self.x, -self.y)
end
function gamera:remove()
love.graphics.pop()
love.graphics.setScissor()
end
function gamera:draw(f)
self:apply()
f(self:getVisible())
self:remove()
end
-- to here
function gamera:toWorld(x,y)
local scale, sin, cos = self.scale, self.sin, self.cos
x,y = (x - self.w2 - self.l) / scale, (y - self.h2 - self.t) / scale
x,y = cos*x - sin*y, sin*x + cos*y
return x + self.x, y + self.y
end
function gamera:toScreen(x,y)
local scale, sin, cos = self.scale, self.sin, self.cos
x,y = x - self.x, y - self.y
x,y = cos*x + sin*y, -sin*x + cos*y
return scale * x + self.w2 + self.l, scale * y + self.h2 + self.t
end
return gamera
+93
View File
@@ -0,0 +1,93 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local function __NULL__() end
-- default gamestate produces error on every callback
local state_init = setmetatable({leave = __NULL__},
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
local stack = {state_init}
local GS = {}
function GS.new(t) return t or {} end -- constructor - deprecated!
function GS.switch(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call switch with colon operator")
local pre = stack[#stack]
;(pre.leave or __NULL__)(pre)
;(to.load or __NULL__)(to)
to.load = nil
stack[#stack] = to
return (to.enter or __NULL__)(to, pre, ...)
end
function GS.push(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call push with colon operator")
local pre = stack[#stack]
;(to.load or __NULL__)(to)
to.load = nil
stack[#stack+1] = to
return (to.enter or __NULL__)(to, pre, ...)
end
function GS.pop(...)
assert(#stack > 1, "No more states to pop!")
local pre, to = stack[#stack], stack[#stack-1]
stack[#stack] = nil
;(pre.leave or __NULL__)(pre)
return (to.resume or __NULL__)(to, pre, ...)
end
function GS.current()
return stack[#stack]
end
local all_callbacks = {
'draw', 'errhand', 'focus', 'keypressed', 'keyreleased', 'mousefocus',
'mousemoved', 'mousepressed', 'mousereleased', 'quit', 'resize',
'textinput', 'threaderror', 'update', 'visible', 'gamepadaxis',
'gamepadpressed', 'gamepadreleased', 'joystickadded', 'joystickaxis',
'joystickhat', 'joystickpressed', 'joystickreleased', 'joystickremoved'
}
function GS.registerEvents(callbacks)
local registry = {}
callbacks = callbacks or all_callbacks
for _, f in ipairs(callbacks) do
registry[f] = love[f] or __NULL__
love[f] = function(...)
registry[f](...)
return GS[f](...)
end
end
end
-- forward any undefined functions
setmetatable(GS, {__index = function(_, func)
return function(...)
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
end
end})
return GS
+91
View File
@@ -0,0 +1,91 @@
--[[
-- multisource lib
-- A multisource is a wrapper on top of a LÖVE source. It allows
-- creating multiple sounds easily. It uses an internal pool of
-- resources.
-- * multisource:play() finds a stopped resource from the pool,
-- or creates a new one, and plays and returns it.
-- * multisouce:cleanup() liberates the memory of old unplayed sources.
]]
local multisource = {}
local MultiSource = {}
local MultiSourceMt = {__index = MultiSource}
function MultiSource:cleanup(older_than)
older_than = older_than or 5
local now = love.timer.getTime()
for instance, lastPlayed in pairs(self.instances) do
local age = now - lastPlayed
if age > older_than and instance:isStopped() then
self.instances[instance] = nil
end
end
end
function MultiSource:getStoppedOrNewInstance()
for instance in pairs(self.instances) do
if instance:isStopped() then return instance end
end
return self.source:clone()
end
function MultiSource:play()
local instance = self:getStoppedOrNewInstance()
self.instances[instance] = love.timer.getTime()
instance:play()
return instance
end
function MultiSource:stop()
for instance in pairs(self.instances) do
instance:stop()
end
end
function MultiSource:pause()
for instance in pairs(self.instances) do
instance:pause()
end
end
function MultiSource:setLooping(looping)
self.source:setLooping(looping)
end
function MultiSource:resume()
for instance in pairs(self.instances) do
instance:resume()
end
end
function MultiSource:countPlayingInstances()
local count = 0
for instance in pairs(self.instances) do
if instance:isPlaying() then
count = count + 1
end
end
return count
end
function MultiSource:countInstances()
local count = 0
for instance in pairs(self.instances) do
count = count + 1
end
return count
end
----
multisource.new = function(source)
return setmetatable({source = source, instances = {}}, MultiSourceMt)
end
return multisource
+1263
View File
File diff suppressed because it is too large Load Diff
+558
View File
@@ -0,0 +1,558 @@
--- @module tiny-ecs
-- @author Calvin Rose
local tiny = { _VERSION = "1.0-2" }
-- Local versions of standard lua functions
local tinsert = table.insert
local tremove = table.remove
local pairs = pairs
local ipairs = ipairs
local setmetatable = setmetatable
local type = type
-- Local versions of the library functions
local tiny_manageEntities
local tiny_manageSystems
local tiny_addEntity
local tiny_addSystem
local tiny_add
local tiny_removeEntity
local tiny_removeSystem
local tiny_remove
--- Filter functions.
-- A Filter is a function that selects which Entities apply to a System.
-- @section Filter
--- Makes a Filter that filters Entities with specified Components.
-- An Entity must have all Components to match the filter.
-- @param ... List of Components
function tiny.requireAll(...)
local components = {...}
local len = #components
return function(_, e)
local c
for i = 1, len do
c = components[i]
if type(c) == 'function' then
if not c(_, e) then
return false
end
elseif e[c] == nil then
return false
end
end
return true
end
end
--- Makes a Filter that filters Entities with specified Components.
-- An Entity must have at least one specified Component to match the filter.
-- @param ... List of Components
function tiny.requireOne(...)
local components = {...}
local len = #components
return function(_, e)
local c
for i = 1, len do
c = components[i]
if type(c) == 'function' then
if c(_, e) then
return true
end
elseif e[c] ~= nil then
return true
end
end
return false
end
end
--- System functions.
-- A System is a wrapper around function callbacks for manipulating Entities.
-- @section System
-- Use an empty table as a key for identifying Systems. Any table that contains
-- this key is considered a System rather than an Entity.
local systemTableKey = { "SYSTEM_TABLE_KEY" }
-- Check if tables are systems.
local function isSystem(table)
return table[systemTableKey]
end
--- Creates a System. Systems are tables that contain at least one field;
-- an update function that takes parameters like so:
-- `function system:update(entities, dt)`. `entities` is an unordered table of
-- Entities with Entities as KEYS, and `dt` is the delta time. There are also a
-- few other optional callbacks:
-- `function system:filter(entity)` - returns a boolean,
-- `function system:onAdd(entity)` - returns nil,
-- `function system:onRemove(entity)` - returns nil.
-- For Filters, it is conveient to use `tiny.requireAll` or `tiny.requireOne`,
-- but one can write their own filters as well.
-- @param table A table to be used as a System, or `nil` to create a new System.
function tiny.system(table)
if table == nil then
table = {}
end
table[systemTableKey] = true
return table
end
-- Update function for all Processing Systems.
local function processingSystemUpdate(system, entities, dt)
local preProcess = system.preProcess
local process = system.process
local postProcess = system.postProcess
local entity
if preProcess then
preProcess(system, entities, dt)
end
if process then
local len = #entities
for i = 1, len do
entity = entities[i]
process(system, entity, dt)
end
end
if postProcess then
postProcess(system, entities, dt)
end
end
--- Creates a Processing System. A Processing System iterates through its
-- Entities in no particluar order, and updates them individually. It has two
-- important fields, `function system:process(entity, dt)`, and `function
-- system:filter(entity)`. `entities` is Entities,
-- and `dt` is the delta time. There are also a few other
-- optional callbacks:
-- `function system:preProcess(entities, dt)` - returns nil,
-- `function system:postProcess(entities, dt)` - returns nil,
-- `function system:onAdd(entity)` - returns nil,
-- `function system:onRemove(entity)` - returns nil.
-- For Filters, it is conveient to use `tiny.requireAll` or `tiny.requireOne`,
-- but one can write their own filters as well.
-- @param table A table to be used as a System, or `nil` to create a new
-- Processing System.
function tiny.processingSystem(table)
if table == nil then
table = {}
end
table[systemTableKey] = true
table.update = processingSystemUpdate
return table
end
--- World functions.
-- A World is a container that manages Entities and Systems. The tiny-ecs module
-- is set to be the `__index` of all World tables, so the often clearer syntax of
-- World:method can be used for any function in the library. For example,
-- `tiny.add(world, e1, e2, e3)` is the same as `world:add(e1, e2, e3).`
-- @section World
local worldMetaTable = { __index = tiny }
--- Creates a new World.
-- Can optionally add default Systems and Entities.
-- @param ... Systems and Entities to add to the World
-- @return A new World
function tiny.world(...)
local ret = {
-- List of Entities to add
entitiesToAdd = {},
-- List of Entities to remove
entitiesToRemove = {},
-- List of Entities to add
systemsToAdd = {},
-- List of Entities to remove
systemsToRemove = {},
-- Set of Entities
entities = {},
-- Number of Entities in World.
entityCount = 0,
-- List of System Data. A data element is a table with 4
-- keys: system, indices, entities, and active.
systems = {},
-- Table of Systems to System Indices
systemIndices = {}
}
tiny_add(ret, ...)
tiny_manageSystems(ret)
tiny_manageEntities(ret)
return setmetatable(ret, worldMetaTable)
end
--- Adds an Entity to the world.
-- The new Entity will enter the world next time World:update is called.
-- Also call this on Entities that have changed Components such that it
-- matches different systems.
-- @param world
-- @param entity
function tiny.addEntity(world, entity)
local e2a = world.entitiesToAdd
e2a[#e2a + 1] = entity
if world.entities[entity] then
tiny_removeEntity(world, entity)
end
end
tiny_addEntity = tiny.addEntity
--- Adds a System to the world.
-- The new System will enter the world next time World:update is called.
-- @param world
-- @param system
function tiny.addSystem(world, system)
local s2a = world.systemsToAdd
s2a[#s2a + 1] = system
end
tiny_addSystem = tiny.addSystem
--- Shortcut for adding multiple Entities and Systems to the World.
-- New objects will enter the World the next time World:update(dt) is called.
-- Also call this method when an Entity has had its Components changed, such
-- that it matches different Filters.
-- @param world
-- @param ... Systems and Entities
function tiny.add(world, ...)
local args = {...}
for _, obj in ipairs(args) do
if isSystem(obj) then
tiny_addSystem(world, obj)
else -- Assume obj is an Entity
tiny_addEntity(world, obj)
end
end
end
tiny_add = tiny.add
--- Removes an Entity to the World.
-- The Entity will exit the World next time World:update is called.
-- Also call this on Entities that have changed Components such that it
-- matches different systems.
-- @param world
-- @param entity
function tiny.removeEntity(world, entity)
local e2r = world.entitiesToRemove
e2r[#e2r + 1] = entity
end
tiny_removeEntity = tiny.removeEntity
--- Removes a System from the world.
-- The System will exit the World next time World:update is called.
-- @param world
-- @param system
function tiny.removeSystem(world, system)
local s2r = world.systemsToRemove
s2r[#s2r + 1] = system
end
tiny_removeSystem = tiny.removeSystem
--- Shortcut for removing multiple Entities and Systems from the World.
-- Objects will exit the World the next time World:update(dt) is called.
-- @param world
-- @param ... Systems and Entities
function tiny.remove(world, ...)
local args = {...}
for _, obj in ipairs(args) do
if isSystem(obj) then
tiny_removeSystem(world, obj)
else -- Assume obj is an Entity
tiny_removeEntity(world, obj)
end
end
end
tiny_remove = tiny.remove
--- Updates a System.
-- @param world
-- @param system A System in the World to update
-- @param dt Delta time
function tiny.updateSystem(world, system, dt)
local es = world.systemEntities[system]
system:update(es, dt)
end
--- Adds and removes Systems that have been marked from the World.
-- The user of this library should seldom if ever call this.
-- @param world
function tiny.manageSystems(world)
local s2a, s2r = world.systemsToAdd, world.systemsToRemove
-- Early exit
if #s2a == 0 and #s2r == 0 then
return
end
local systemIndices = world.systemIndices
local entities = world.entities
local systems = world.systems
local system, systemData, index, filter, entityList, entityIndices, entityIndex, onRemove, onAdd
-- Remove Systems
for i = 1, #s2r do
system = s2r[i]
index = systemIndices[system]
if index then
systemData = systems[index]
onRemove = system.onRemove
if onRemove then
entityList = systemData.entities
for j = 1, #entityList do
onRemove(system, entityList[j])
end
end
systemIndices[system] = nil
tremove(systems, index)
for j = index, #systems do
systemIndices[systems[j].system] = j
end
end
s2r[i] = nil
end
-- Add Systems
for i = 1, #s2a do
system = s2a[i]
if not systemIndices[system] then
entityList = {}
entityIndices = {}
systemData = { system = system, entities = entityList, indices = entityIndices, active = true }
index = #systems + 1
systemIndices[system] = index
systems[index] = systemData
-- Try to add Entities
onAdd = system.onAdd
filter = system.filter
if filter then
for entity in pairs(entities) do
if filter(system, entity) then
entityIndex = #entityList + 1
entityList[entityIndex] = entity
entityIndices[entity] = entityIndex
if onAdd then
onAdd(system, entity)
end
end
end
end
end
s2a[i] = nil
end
end
tiny_manageSystems = tiny.manageSystems
--- Adds and removes Entities that have been marked.
-- The user of this library should seldom if ever call this.
-- @param world
function tiny.manageEntities(world)
local e2a, e2r = world.entitiesToAdd, world.entitiesToRemove
-- Early exit
if #e2a == 0 and #e2r == 0 then
return
end
local entities = world.entities
local systems = world.systems
local entityCount = world.entityCount
local entity, system, systemData, index, onRemove, onAdd, ses, seis, filter, tmpEntity
-- Remove Entities
for i = 1, #e2r do
entity = e2r[i]
if entities[entity] then
entities[entity] = nil
for j = 1, #systems do
systemData = systems[j]
system = systemData.system
ses = systemData.entities
seis = systemData.indices
index = seis[entity]
if index then
tmpEntity = ses[#ses]
ses[index] = tmpEntity
seis[tmpEntity] = index
seis[entity] = nil
ses[#ses] = nil
entityCount = entityCount - 1
onRemove = system.onRemove
if onRemove then
onRemove(system, entity)
end
end
end
end
e2r[i] = nil
end
-- Add Entities
for i = 1, #e2a do
entity = e2a[i]
if not entities[entity] then
entities[entity] = true
for j = 1, #systems do
systemData = systems[j]
system = systemData.system
ses = systemData.entities
seis = systemData.indices
filter = system.filter
if filter and filter(system, entity) then
index = #ses + 1
ses[index] = entity
seis[entity] = index
entityCount = entityCount + 1
onAdd = system.onAdd
if onAdd then
onAdd(system, entity)
end
end
end
end
e2a[i] = nil
end
-- Update Entity count
world.entityCount = entityCount
end
tiny_manageEntities = tiny.manageEntities
--- Updates the World.
-- Frees Entities that have been marked for freeing, adds
-- entities that have been marked for adding, etc.
-- @param world
-- @param dt Delta time
function tiny.update(world, dt)
tiny_manageSystems(world)
tiny_manageEntities(world)
local systems = world.systems
local systemData
-- Iterate through Systems IN ORDER
for i = 1, #systems do
systemData = systems[i]
if systemData.active then
systemData.system:update(systemData.entities, dt)
end
end
end
--- Removes all Entities from the World.
-- When World:update(dt) is next called,
-- all Entities will be removed.
-- @param world
function tiny.clearEntities(world)
for e in pairs(world.entities) do
tiny_removeEntity(world, e)
end
end
--- Removes all Systems from the World.
-- When World:update(dt) is next called,
-- all Systems will be removed.
-- @param world
function tiny.clearSystems(world)
local systems = world.systems
for i = #systems, 1, -1 do
tiny_removeSystem(world, systems[i].system)
end
end
--- Gets count of Entities in World.
-- @param world
function tiny.getEntityCount(world)
return world.entityCount
end
--- Gets count of Systems in World.
-- @param world
function tiny.getSystemCount(world)
return #(world.systems)
end
--- Gets the index of a System in the world. Lower indexed Systems are processed
-- before higher indexed systems.
-- @param world
-- @param system
function tiny.getSystemIndex(world, system)
return world.systemIndices[system]
end
--- Sets the index of a System in the world. Changes the order in
-- which they Systems processed, because lower indexed Systems are processed
-- first.
-- @param world
-- @param system
-- @param index
function tiny.setSystemIndex(world, system, index)
local systemIndices = world.systemIndices
local oldIndex = systemIndices[system]
local systems = world.systems
local systemData = systems[oldIndex]
tremove(systems, oldIndex)
tinsert(systems, index, systemData)
for i = oldIndex, index, index >= oldIndex and 1 or -1 do
systemIndices[systems[i].system] = i
end
end
--- Activates Systems in the World.
-- Activated Systems will be update whenever tiny.update(world, dt) is called.
-- @param world
-- @param ... Systems to activate. The Systems must already be added to the
-- World.
function tiny.activate(world, ...)
local args = {...}
for _, system in ipairs(args) do
world.systems[world.systemIndices[system]].active = true
end
end
--- Deactivates Systems in the World.
-- Deactivated Systems must be update manually, and will not update when the
-- rest of World updates. They will, however, process new Entities added while
-- the System is deactivated.
-- @param world
-- @param ... Systems to deactivate. The Systems must already be added to the
-- World.
function tiny.deactivate(world, ...)
local args = {...}
for _, system in ipairs(args) do
world.systems[world.systemIndices[system]].active = false
end
end
return tiny