add slick, add top down race
This commit is contained in:
parent
1dc045cbed
commit
76e61ed233
22
game/love_src/lib/slick/cache.lua
Normal file
22
game/love_src/lib/slick/cache.lua
Normal file
@ -0,0 +1,22 @@
|
||||
local delaunay = require("slick.geometry.triangulation.delaunay")
|
||||
|
||||
--- @class slick.cache
|
||||
--- @field triangulator slick.geometry.triangulation.delaunay
|
||||
local cache = {}
|
||||
local metatable = { __index = cache }
|
||||
|
||||
--- @param options slick.options
|
||||
function cache.new(options)
|
||||
if options.sharedCache then
|
||||
return options.sharedCache
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
triangulator = delaunay.new({
|
||||
epsilon = options.epsilon,
|
||||
debug = options.debug
|
||||
})
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
return cache
|
||||
50
game/love_src/lib/slick/collision/box.lua
Normal file
50
game/love_src/lib/slick/collision/box.lua
Normal file
@ -0,0 +1,50 @@
|
||||
local commonShape = require("slick.collision.commonShape")
|
||||
local transform = require("slick.geometry.transform")
|
||||
|
||||
--- @class slick.collision.box: slick.collision.commonShape
|
||||
local box = setmetatable({}, { __index = commonShape })
|
||||
local metatable = { __index = box }
|
||||
|
||||
--- @param entity slick.entity | slick.cache | nil
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param w number
|
||||
--- @param h number
|
||||
--- @return slick.collision.box
|
||||
function box.new(entity, x, y, w, h)
|
||||
local result = setmetatable(commonShape.new(entity), metatable)
|
||||
|
||||
--- @cast result slick.collision.box
|
||||
result:init(x, y, w, h)
|
||||
return result
|
||||
end
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param w number
|
||||
--- @param h number
|
||||
function box:init(x, y, w, h)
|
||||
commonShape.init(self)
|
||||
|
||||
self:addPoints(
|
||||
x, y,
|
||||
x + w, y,
|
||||
x + w, y + h,
|
||||
x, y + h)
|
||||
|
||||
self:addNormal(0, 1)
|
||||
self:addNormal(-1, 0)
|
||||
self:addNormal(0, -1)
|
||||
self:addNormal(1, 0)
|
||||
|
||||
self:transform(transform.IDENTITY)
|
||||
|
||||
assert(self.vertexCount == 4, "box must have 4 points")
|
||||
assert(self.normalCount == 4, "box must have 4 normals")
|
||||
end
|
||||
|
||||
function box:inside(p)
|
||||
return p.x >= self.bounds:left() and p.x <= self.bounds:right() and p.y >= self.bounds:top() and p.y <= self.bounds:bottom()
|
||||
end
|
||||
|
||||
return box
|
||||
288
game/love_src/lib/slick/collision/commonShape.lua
Normal file
288
game/love_src/lib/slick/collision/commonShape.lua
Normal file
@ -0,0 +1,288 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.collision.commonShape
|
||||
--- @field tag any
|
||||
--- @field entity slick.entity | slick.cache | nil
|
||||
--- @field vertexCount number
|
||||
--- @field normalCount number
|
||||
--- @field center slick.geometry.point
|
||||
--- @field vertices slick.geometry.point[]
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field private preTransformedVertices slick.geometry.point[]
|
||||
--- @field normals slick.geometry.point[]
|
||||
--- @field private preTransformedNormals slick.geometry.point[]
|
||||
local commonShape = {}
|
||||
local metatable = { __index = commonShape }
|
||||
|
||||
--- @param e slick.entity | slick.cache | nil
|
||||
--- @return slick.collision.commonShape
|
||||
function commonShape.new(e)
|
||||
return setmetatable({
|
||||
entity = e,
|
||||
vertexCount = 0,
|
||||
normalCount = 0,
|
||||
center = point.new(),
|
||||
bounds = rectangle.new(),
|
||||
vertices = {},
|
||||
preTransformedVertices = {},
|
||||
normals = {},
|
||||
preTransformedNormals = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
function commonShape:init()
|
||||
self.vertexCount = 0
|
||||
self.normalCount = 0
|
||||
end
|
||||
|
||||
function commonShape:makeClockwise()
|
||||
-- Line segments don't have a winding.
|
||||
-- And points nor empty polygons do either.
|
||||
if self.vertexCount < 3 then
|
||||
return
|
||||
end
|
||||
|
||||
local winding
|
||||
for i = 1, self.vertexCount do
|
||||
local j = slickmath.wrap(i, 1, self.vertexCount)
|
||||
local k = slickmath.wrap(j, 1, self.vertexCount)
|
||||
|
||||
local side = slickmath.direction(
|
||||
self.preTransformedVertices[i],
|
||||
self.preTransformedVertices[j],
|
||||
self.preTransformedVertices[k])
|
||||
|
||||
if side ~= 0 then
|
||||
winding = side
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not winding then
|
||||
return
|
||||
end
|
||||
|
||||
if winding <= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local i = self.vertexCount
|
||||
local j = 1
|
||||
|
||||
while i > j do
|
||||
self.preTransformedVertices[i], self.preTransformedVertices[j] = self.preTransformedVertices[j], self.preTransformedVertices[i]
|
||||
|
||||
i = i - 1
|
||||
j = j + 1
|
||||
end
|
||||
end
|
||||
|
||||
--- @protected
|
||||
--- @param x1 number?
|
||||
--- @param y1 number?
|
||||
--- @param ... number?
|
||||
function commonShape:addPoints(x1, y1, ...)
|
||||
if not (x1 and y1) then
|
||||
self:makeClockwise()
|
||||
return
|
||||
end
|
||||
|
||||
self.vertexCount = self.vertexCount + 1
|
||||
local p = self.preTransformedVertices[self.vertexCount]
|
||||
if not p then
|
||||
p = point.new()
|
||||
table.insert(self.preTransformedVertices, p)
|
||||
end
|
||||
p:init(x1, y1)
|
||||
|
||||
self:addPoints(...)
|
||||
end
|
||||
|
||||
--- @protected
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function commonShape:addNormal(x, y)
|
||||
assert(not (x == 0 and y == 0))
|
||||
|
||||
self.normalCount = self.normalCount + 1
|
||||
|
||||
local normal = self.preTransformedNormals[self.normalCount]
|
||||
if not normal then
|
||||
normal = point.new()
|
||||
self.preTransformedNormals[self.normalCount] = normal
|
||||
end
|
||||
|
||||
normal:init(x, y)
|
||||
normal:normalize(normal)
|
||||
|
||||
return normal
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @return boolean
|
||||
function commonShape:inside(p)
|
||||
local winding
|
||||
for i = 1, self.vertexCount do
|
||||
local side = slickmath.direction(self.vertices[i], self.vertices[i % self.vertexCount + 1], p)
|
||||
|
||||
-- Point is collinear with edge.
|
||||
-- We consider this inside.
|
||||
if side == 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
if winding and side ~= winding then
|
||||
return false
|
||||
end
|
||||
|
||||
if not winding and side ~= 0 then
|
||||
winding = side
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local _cachedNormal = point.new()
|
||||
function commonShape:buildNormals()
|
||||
local direction = slickmath.direction(self.preTransformedVertices[1], self.preTransformedVertices[2], self.preTransformedVertices[3])
|
||||
assert(direction ~= 0, "polygon is degenerate")
|
||||
if direction < 0 then
|
||||
slicktable.reverse(self.preTransformedVertices)
|
||||
end
|
||||
|
||||
|
||||
for i = 1, self.vertexCount do
|
||||
local j = i % self.vertexCount + 1
|
||||
|
||||
local p1 = self.preTransformedVertices[i]
|
||||
local p2 = self.preTransformedVertices[j]
|
||||
|
||||
p1:direction(p2, _cachedNormal)
|
||||
|
||||
local n = self:addNormal(_cachedNormal.x, _cachedNormal.y)
|
||||
n:right(n)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param transform slick.geometry.transform
|
||||
function commonShape:transform(transform)
|
||||
self.center:init(0, 0)
|
||||
|
||||
for i = 1, self.vertexCount do
|
||||
local preTransformedVertex = self.preTransformedVertices[i]
|
||||
local postTransformedVertex = self.vertices[i]
|
||||
if not postTransformedVertex then
|
||||
postTransformedVertex = point.new()
|
||||
self.vertices[i] = postTransformedVertex
|
||||
end
|
||||
postTransformedVertex:init(transform:transformPoint(preTransformedVertex.x, preTransformedVertex.y))
|
||||
|
||||
postTransformedVertex:add(self.center, self.center)
|
||||
end
|
||||
self.center:divideScalar(self.vertexCount, self.center)
|
||||
|
||||
self.bounds:init(self.vertices[1].x, self.vertices[1].y, self.vertices[1].x, self.vertices[1].y)
|
||||
for i = 2, self.vertexCount do
|
||||
self.bounds:expand(self.vertices[i].x, self.vertices[i].y)
|
||||
end
|
||||
|
||||
for i = 1, self.normalCount do
|
||||
local preTransformedNormal = self.preTransformedNormals[i]
|
||||
local postTransformedNormal = self.normals[i]
|
||||
if not postTransformedNormal then
|
||||
postTransformedNormal = point.new()
|
||||
self.normals[i] = postTransformedNormal
|
||||
end
|
||||
postTransformedNormal:init(transform:transformNormal(preTransformedNormal.x, preTransformedNormal.y))
|
||||
postTransformedNormal:normalize(postTransformedNormal)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param query slick.collision.shapeCollisionResolutionQuery
|
||||
function commonShape:getAxes(query)
|
||||
query:getAxes()
|
||||
end
|
||||
|
||||
--- @param query slick.collision.shapeCollisionResolutionQuery
|
||||
--- @param axis slick.geometry.point
|
||||
--- @param interval slick.collision.interval
|
||||
--- @param offset slick.geometry.point?
|
||||
function commonShape:project(query, axis, interval, offset)
|
||||
query:project(axis, interval, offset)
|
||||
end
|
||||
|
||||
local _cachedDistanceSegment = segment.new()
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
function commonShape:distance(p)
|
||||
if self:inside(p) then
|
||||
return 0
|
||||
end
|
||||
|
||||
local minDistance = math.huge
|
||||
|
||||
for i = 1, self.vertexCount do
|
||||
local j = i % self.vertexCount + 1
|
||||
|
||||
_cachedDistanceSegment:init(self.vertices[i], self.vertices[j])
|
||||
local distanceSquared = _cachedDistanceSegment:distanceSquared(p)
|
||||
if distanceSquared < minDistance then
|
||||
minDistance = distanceSquared
|
||||
end
|
||||
end
|
||||
|
||||
if minDistance < math.huge then
|
||||
return math.sqrt(minDistance)
|
||||
end
|
||||
|
||||
return math.huge
|
||||
end
|
||||
|
||||
local _cachedRaycastHit = point.new()
|
||||
local _cachedRaycastSegment = segment.new()
|
||||
|
||||
--- @param r slick.geometry.ray
|
||||
--- @param normal slick.geometry.point?
|
||||
--- @return boolean, number?, number?
|
||||
function commonShape:raycast(r, normal)
|
||||
local bestDistance = math.huge
|
||||
local hit, x, y
|
||||
|
||||
for i = 1, self.vertexCount do
|
||||
local j = i % self.vertexCount + 1
|
||||
|
||||
local a = self.vertices[i]
|
||||
local b = self.vertices[j]
|
||||
|
||||
_cachedRaycastSegment:init(a, b)
|
||||
local h, hx, hy = r:hitSegment(_cachedRaycastSegment)
|
||||
if h and hx and hy then
|
||||
hit = true
|
||||
|
||||
_cachedRaycastHit:init(hx, hy)
|
||||
local distance = _cachedRaycastHit:distanceSquared(r.origin)
|
||||
if distance < bestDistance then
|
||||
bestDistance = distance
|
||||
x, y = hx, hy
|
||||
|
||||
if normal then
|
||||
a:direction(b, normal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if normal and hit then
|
||||
normal:normalize(normal)
|
||||
normal:left(normal)
|
||||
end
|
||||
|
||||
return hit or false, x, y
|
||||
end
|
||||
|
||||
return commonShape
|
||||
14
game/love_src/lib/slick/collision/init.lua
Normal file
14
game/love_src/lib/slick/collision/init.lua
Normal file
@ -0,0 +1,14 @@
|
||||
--- @alias slick.collision.shapeInterface slick.collision.commonShape
|
||||
|
||||
--- @alias slick.collision.shape slick.collision.polygon | slick.collision.box | slick.collision.commonShape
|
||||
--- @alias slick.collision.shapelike slick.collision.shape | slick.collision.shapeGroup | slick.collision.shapeInterface | slick.collision.polygonMesh
|
||||
|
||||
local collision = {
|
||||
quadTree = require("slick.collision.quadTree"),
|
||||
quadTreeNode = require("slick.collision.quadTreeNode"),
|
||||
quadTreeQuery = require("slick.collision.quadTreeQuery"),
|
||||
polygon = require("slick.collision.polygon"),
|
||||
shapeCollisionResolutionQuery = require("slick.collision.shapeCollisionResolutionQuery"),
|
||||
}
|
||||
|
||||
return collision
|
||||
119
game/love_src/lib/slick/collision/interval.lua
Normal file
119
game/love_src/lib/slick/collision/interval.lua
Normal file
@ -0,0 +1,119 @@
|
||||
local slicktable = require "slick.util.slicktable"
|
||||
|
||||
--- @alias slick.collision.intervalIndex {
|
||||
--- value: number,
|
||||
--- index: number,
|
||||
--- }
|
||||
|
||||
--- @class slick.collision.interval
|
||||
--- @field min number
|
||||
--- @field max number
|
||||
--- @field indexCount number
|
||||
--- @field indices slick.collision.intervalIndex[]
|
||||
--- @field private indicesCache slick.collision.intervalIndex[]
|
||||
local interval = {}
|
||||
local metatable = { __index = interval }
|
||||
|
||||
--- @return slick.collision.interval
|
||||
function interval.new()
|
||||
return setmetatable({ indices = {}, indicesCache = {}, minIndex = 0, maxIndex = 0 }, metatable)
|
||||
end
|
||||
|
||||
function interval:init()
|
||||
self.min = nil
|
||||
self.max = nil
|
||||
self.minIndex = 0
|
||||
self.maxIndex = 0
|
||||
slicktable.clear(self.indices)
|
||||
end
|
||||
|
||||
--- @return number
|
||||
--- @return number
|
||||
function interval:get()
|
||||
return self.min or 0, self.max or 0
|
||||
end
|
||||
|
||||
--- @param value number
|
||||
function interval:update(value, index)
|
||||
self.min = math.min(self.min or value, value)
|
||||
self.max = math.max(self.max or value, value)
|
||||
|
||||
local i = #self.indices + 1
|
||||
local indexInfo = self.indicesCache[i]
|
||||
if not indexInfo then
|
||||
indexInfo = {}
|
||||
self.indicesCache[i] = indexInfo
|
||||
end
|
||||
|
||||
indexInfo.index = index
|
||||
indexInfo.value = value
|
||||
|
||||
table.insert(self.indices, indexInfo)
|
||||
end
|
||||
|
||||
local function _lessIntervalIndex(a, b)
|
||||
return a.value < b.value
|
||||
end
|
||||
|
||||
function interval:sort()
|
||||
table.sort(self.indices, _lessIntervalIndex)
|
||||
|
||||
for i, indexInfo in ipairs(self.indices) do
|
||||
if indexInfo.value >= self.min and self.minIndex == 0 then
|
||||
self.minIndex = i
|
||||
end
|
||||
|
||||
if indexInfo.value <= self.max and indexInfo.value >= self.min then
|
||||
self.maxIndex = i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param other slick.collision.interval
|
||||
function interval:copy(other)
|
||||
other.min = self.min
|
||||
other.max = self.max
|
||||
other.minIndex = self.minIndex
|
||||
other.maxIndex = self.maxIndex
|
||||
|
||||
slicktable.clear(other.indices)
|
||||
for i, selfIndexInfo in ipairs(self.indices) do
|
||||
local otherIndexInfo = other.indicesCache[i]
|
||||
if not otherIndexInfo then
|
||||
otherIndexInfo = {}
|
||||
table.insert(other.indicesCache, otherIndexInfo)
|
||||
end
|
||||
|
||||
otherIndexInfo.index = selfIndexInfo.index
|
||||
otherIndexInfo.value = selfIndexInfo.value
|
||||
|
||||
table.insert(other.indices, otherIndexInfo)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param min number
|
||||
--- @param max number
|
||||
function interval:set(min, max)
|
||||
assert(min <= max)
|
||||
|
||||
self.min = min
|
||||
self.max = max
|
||||
end
|
||||
|
||||
function interval:overlaps(other)
|
||||
return not (self.min > other.max or other.min > self.max)
|
||||
end
|
||||
|
||||
function interval:distance(other)
|
||||
if self:overlaps(other) then
|
||||
return math.min(self.max, other.max) - math.max(self.min, other.min)
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function interval:contains(other)
|
||||
return other.min > self.min and other.max < self.max
|
||||
end
|
||||
|
||||
return interval
|
||||
94
game/love_src/lib/slick/collision/lineSegment.lua
Normal file
94
game/love_src/lib/slick/collision/lineSegment.lua
Normal file
@ -0,0 +1,94 @@
|
||||
local commonShape = require("slick.collision.commonShape")
|
||||
local point = require("slick.geometry.point")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local transform = require("slick.geometry.transform")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.collision.lineSegment: slick.collision.commonShape
|
||||
--- @field segment slick.geometry.segment
|
||||
--- @field private preTransformedSegment slick.geometry.segment
|
||||
local lineSegment = setmetatable({}, { __index = commonShape })
|
||||
local metatable = { __index = lineSegment }
|
||||
|
||||
--- @param entity slick.entity?
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @return slick.collision.lineSegment
|
||||
function lineSegment.new(entity, x1, y1, x2, y2)
|
||||
local result = setmetatable(commonShape.new(entity), metatable)
|
||||
|
||||
result.segment = segment.new()
|
||||
result.preTransformedSegment = segment.new()
|
||||
|
||||
--- @cast result slick.collision.lineSegment
|
||||
result:init(x1, y1, x2, y2)
|
||||
return result
|
||||
end
|
||||
|
||||
local _cachedInitNormal = point.new()
|
||||
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
function lineSegment:init(x1, y1, x2, y2)
|
||||
commonShape.init(self)
|
||||
|
||||
self.preTransformedSegment.a:init(x1, y1)
|
||||
self.preTransformedSegment.b:init(x2, y2)
|
||||
|
||||
if not self.preTransformedSegment.a:lessThanOrEqual(self.preTransformedSegment.b) then
|
||||
self.preTransformedSegment.a, self.preTransformedSegment.b = self.preTransformedSegment.b, self.preTransformedSegment.a
|
||||
end
|
||||
|
||||
self:addPoints(
|
||||
self.preTransformedSegment.a.x,
|
||||
self.preTransformedSegment.a.y,
|
||||
self.preTransformedSegment.b.x,
|
||||
self.preTransformedSegment.b.y)
|
||||
|
||||
self.preTransformedSegment.a:direction(self.preTransformedSegment.b, _cachedInitNormal)
|
||||
_cachedInitNormal:normalize(_cachedInitNormal)
|
||||
|
||||
self:addNormal(_cachedInitNormal.x, _cachedInitNormal.y)
|
||||
|
||||
_cachedInitNormal:left(_cachedInitNormal)
|
||||
self:addNormal(_cachedInitNormal.x, _cachedInitNormal.y)
|
||||
|
||||
self:transform(transform.IDENTITY)
|
||||
|
||||
assert(self.vertexCount == 2, "line segment must have 2 points")
|
||||
assert(self.normalCount == 2, "line segment must have 2 normals")
|
||||
end
|
||||
|
||||
--- @param transform slick.geometry.transform
|
||||
function lineSegment:transform(transform)
|
||||
commonShape.transform(self, transform)
|
||||
|
||||
self.segment.a:init(transform:transformPoint(self.preTransformedSegment.a.x, self.preTransformedSegment.a.y))
|
||||
self.segment.b:init(transform:transformPoint(self.preTransformedSegment.b.x, self.preTransformedSegment.b.y))
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @return boolean
|
||||
function lineSegment:inside(p)
|
||||
local intersection, x, y = slickmath.intersection(self.vertices[1], self.vertices[2], p, p)
|
||||
return intersection and not (x and y)
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @return number
|
||||
function lineSegment:distance(p)
|
||||
return self.segment:distance(p)
|
||||
end
|
||||
|
||||
--- @param r slick.geometry.ray
|
||||
--- @return boolean, number?, number?
|
||||
function lineSegment:raycast(r)
|
||||
local h, x, y = r:hitSegment(self.segment)
|
||||
return h, x, y
|
||||
end
|
||||
|
||||
return lineSegment
|
||||
46
game/love_src/lib/slick/collision/polygon.lua
Normal file
46
game/love_src/lib/slick/collision/polygon.lua
Normal file
@ -0,0 +1,46 @@
|
||||
local commonShape = require("slick.collision.commonShape")
|
||||
local transform = require("slick.geometry.transform")
|
||||
|
||||
--- @class slick.collision.polygon: slick.collision.commonShape
|
||||
local polygon = setmetatable({}, { __index = commonShape })
|
||||
local metatable = { __index = polygon }
|
||||
|
||||
--- @param entity slick.entity | slick.cache
|
||||
--- @param x1 number?
|
||||
--- @param y1 number?
|
||||
--- @param x2 number?
|
||||
--- @param y2 number?
|
||||
--- @param x3 number?
|
||||
--- @param y3 number?
|
||||
--- @param ... number
|
||||
--- @return slick.collision.polygon
|
||||
function polygon.new(entity, x1, y1, x2, y2, x3, y3, ...)
|
||||
local result = setmetatable(commonShape.new(entity), metatable)
|
||||
--- @cast result slick.collision.polygon
|
||||
|
||||
if x1 and y1 and x2 and y2 and x3 and y3 then
|
||||
result:init(x1, y1, x2, y2, x3, y3, ...)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @param x3 number
|
||||
--- @param y3 number
|
||||
--- @param ... number
|
||||
function polygon:init(x1, y1, x2, y2, x3, y3, ...)
|
||||
commonShape.init(self)
|
||||
|
||||
self:addPoints(x1, y1, x2, y2, x3, y3, ...)
|
||||
self:buildNormals()
|
||||
self:transform(transform.IDENTITY)
|
||||
|
||||
assert(self.vertexCount >= 3, "polygon must have at least 3 points")
|
||||
assert(self.vertexCount == self.normalCount, "polygon must have as many normals as vertices")
|
||||
end
|
||||
|
||||
return polygon
|
||||
77
game/love_src/lib/slick/collision/polygonMesh.lua
Normal file
77
game/love_src/lib/slick/collision/polygonMesh.lua
Normal file
@ -0,0 +1,77 @@
|
||||
local polygon = require ("slick.collision.polygon")
|
||||
|
||||
--- @class slick.collision.polygonMesh
|
||||
--- @field tag any
|
||||
--- @field entity slick.entity
|
||||
--- @field boundaries number[][]
|
||||
--- @field polygons slick.collision.polygon[]
|
||||
--- @field cleanupOptions slick.geometry.triangulation.delaunayCleanupOptions
|
||||
--- @field triangulationOptions slick.geometry.triangulation.delaunayTriangulationOptions
|
||||
local polygonMesh = {}
|
||||
local metatable = { __index = polygonMesh }
|
||||
|
||||
--- @param entity slick.entity
|
||||
--- @param ... number[]
|
||||
--- @return slick.collision.polygonMesh
|
||||
function polygonMesh.new(entity, ...)
|
||||
local result = setmetatable({
|
||||
entity = entity,
|
||||
boundaries = { ... },
|
||||
polygons = {},
|
||||
cleanupOptions = {},
|
||||
triangulationOptions = {
|
||||
refine = true,
|
||||
interior = true,
|
||||
exterior = false,
|
||||
polygonization = true
|
||||
}
|
||||
}, metatable)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @param triangulator slick.geometry.triangulation.delaunay
|
||||
function polygonMesh:build(triangulator)
|
||||
local points = {}
|
||||
local edges = {}
|
||||
|
||||
local totalPoints = 0
|
||||
for _, boundary in ipairs(self.boundaries) do
|
||||
local numPoints = #boundary / 2
|
||||
|
||||
for i = 1, numPoints do
|
||||
local j = (i - 1) * 2 + 1
|
||||
local x, y = unpack(boundary, j, j + 1)
|
||||
|
||||
table.insert(points, x)
|
||||
table.insert(points, y)
|
||||
table.insert(edges, i + totalPoints)
|
||||
table.insert(edges, i % numPoints + 1 + totalPoints)
|
||||
end
|
||||
|
||||
totalPoints = totalPoints + numPoints
|
||||
end
|
||||
|
||||
points, edges = triangulator:clean(points, edges, self.cleanupOptions)
|
||||
local triangles, _, polygons = triangulator:triangulate(points, edges, self.triangulationOptions)
|
||||
|
||||
local p = polygons or triangles
|
||||
for _, vertices in ipairs(p) do
|
||||
local outputVertices = {}
|
||||
|
||||
for _, vertex in ipairs(vertices) do
|
||||
local index = (vertex - 1) * 2 + 1
|
||||
local x, y = unpack(points, index, index + 1)
|
||||
|
||||
table.insert(outputVertices, x)
|
||||
table.insert(outputVertices, y)
|
||||
end
|
||||
|
||||
local instantiatedPolygon = polygon.new(self.entity, unpack(outputVertices))
|
||||
instantiatedPolygon.tag = self.tag
|
||||
|
||||
table.insert(self.polygons, instantiatedPolygon)
|
||||
end
|
||||
end
|
||||
|
||||
return polygonMesh
|
||||
283
game/love_src/lib/slick/collision/quadTree.lua
Normal file
283
game/love_src/lib/slick/collision/quadTree.lua
Normal file
@ -0,0 +1,283 @@
|
||||
local quadTreeNode = require("slick.collision.quadTreeNode")
|
||||
local point = require("slick.geometry.point")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local util = require("slick.util")
|
||||
local pool = require("slick.util.pool")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.collision.quadTree
|
||||
--- @field root slick.collision.quadTreeNode
|
||||
--- @field expand boolean
|
||||
--- @field private depth number? **internal**
|
||||
--- @field private nodesPool slick.util.pool **internal**
|
||||
--- @field private rectanglesPool slick.util.pool **internal**
|
||||
--- @field private data table<any, slick.geometry.rectangle> **internal**
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field maxLevels number
|
||||
--- @field maxData number
|
||||
local quadTree = {}
|
||||
local metatable = { __index = quadTree }
|
||||
|
||||
--- @class slick.collision.quadTreeOptions
|
||||
--- @field x number?
|
||||
--- @field y number?
|
||||
--- @field width number?
|
||||
--- @field height number?
|
||||
--- @field maxLevels number?
|
||||
--- @field maxData number?
|
||||
--- @field expand boolean?
|
||||
local defaultOptions = {
|
||||
x = -1024,
|
||||
y = -1024,
|
||||
width = 2048,
|
||||
height = 2048,
|
||||
maxLevels = 8,
|
||||
maxData = 8,
|
||||
expand = true
|
||||
}
|
||||
|
||||
--- @param options slick.collision.quadTreeOptions?
|
||||
--- @return slick.collision.quadTree
|
||||
function quadTree.new(options)
|
||||
options = options or defaultOptions
|
||||
|
||||
assert((options.width or defaultOptions.width) > 0, "width must be greater than 0")
|
||||
assert((options.height or defaultOptions.height) > 0, "height must be greater than 0")
|
||||
|
||||
local result = setmetatable({
|
||||
maxLevels = options.maxLevels or defaultOptions.maxLevels,
|
||||
maxData = options.maxData or defaultOptions.maxData,
|
||||
expand = options.expand == nil and defaultOptions.expand or not not options.expand,
|
||||
depth = 1,
|
||||
bounds = rectangle.new(
|
||||
(options.x or defaultOptions.x),
|
||||
(options.y or defaultOptions.y),
|
||||
(options.x or defaultOptions.x) + (options.width or defaultOptions.width),
|
||||
(options.y or defaultOptions.y) + (options.height or defaultOptions.height)
|
||||
),
|
||||
nodesPool = pool.new(quadTreeNode),
|
||||
rectanglesPool = pool.new(rectangle),
|
||||
data = {}
|
||||
}, metatable)
|
||||
|
||||
result.root = result:_newNode(nil, result:left(), result:top(), result:right(), result:bottom())
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Rebuilds the quad tree with the new options, preserving all existing data.
|
||||
--- All options will be set to the new options, if set.
|
||||
--- @param options slick.collision.quadTreeOptions
|
||||
function quadTree:rebuild(options)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self.root:_snip() -- this method is internal, not private
|
||||
self.nodesPool:deallocate(self.root)
|
||||
|
||||
assert(options.width == nil or options.width > 0, "width must be greater than 0")
|
||||
assert(options.height == nil or options.height > 0, "height must be greater than 0")
|
||||
|
||||
local x = options.x or self.bounds:left()
|
||||
local y = options.y or self.bounds:top()
|
||||
local width = options.width or self.bounds:width()
|
||||
local height = options.height or self.bounds:height()
|
||||
|
||||
self.maxLevels = options.maxLevels or self.maxLevels
|
||||
self.maxData = options.maxData or self.maxData
|
||||
|
||||
if options.expand ~= nil then
|
||||
self.expand = options.expand
|
||||
end
|
||||
|
||||
self.bounds:init(x, y, x + width, y + height)
|
||||
self.root = self:_newNode(nil, self.bounds:left(), self.bounds:top(), self.bounds:right(), self.bounds:bottom())
|
||||
|
||||
for data, r in pairs(self.data) do
|
||||
self:_tryExpand(r)
|
||||
self.root:insert(data, r)
|
||||
end
|
||||
end
|
||||
|
||||
function quadTree:clear()
|
||||
for data, r in pairs(self.data) do
|
||||
self.root:remove(data, r)
|
||||
self.rectanglesPool:deallocate(r)
|
||||
end
|
||||
|
||||
slicktable.clear(self.data)
|
||||
end
|
||||
|
||||
--- Returns the exact bounds of all data in the quad tree.
|
||||
--- @return number x1
|
||||
--- @return number y1
|
||||
--- @return number x2
|
||||
--- @return number y2
|
||||
function quadTree:computeExactBounds()
|
||||
local left, right, top, bottom
|
||||
|
||||
for _, r in pairs(self.data) do
|
||||
left = math.min(left or r:left(), r:left())
|
||||
right = math.max(right or r:right(), r:right())
|
||||
top = math.min(top or r:top(), r:top())
|
||||
bottom = math.max(bottom or r:bottom(), r:bottom())
|
||||
end
|
||||
|
||||
return left, top, right, bottom
|
||||
end
|
||||
|
||||
--- Returns the maximum left of the quad tree.
|
||||
--- @return number
|
||||
function quadTree:left()
|
||||
return self.bounds:left()
|
||||
end
|
||||
|
||||
--- Returns the maximum right of the quad tree.
|
||||
--- @return number
|
||||
function quadTree:right()
|
||||
return self.bounds:right()
|
||||
end
|
||||
|
||||
--- Returns the maximum top of the quad tree.
|
||||
--- @return number
|
||||
function quadTree:top()
|
||||
return self.bounds:top()
|
||||
end
|
||||
|
||||
--- Returns the maximum bottom of the quad tree.
|
||||
--- @return number
|
||||
function quadTree:bottom()
|
||||
return self.bounds:bottom()
|
||||
end
|
||||
|
||||
--- Returns true if quad tree has `data`
|
||||
--- @param data any
|
||||
--- @return boolean
|
||||
function quadTree:has(data)
|
||||
return self.data[data] ~= nil
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @return slick.geometry.rectangle
|
||||
function quadTree:_newRectangle(x1, y1, x2, y2)
|
||||
--- @type slick.geometry.rectangle
|
||||
return self.rectanglesPool:allocate(x1, y1, x2, y2)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param parent slick.collision.quadTreeNode?
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @return slick.collision.quadTreeNode
|
||||
function quadTree:_newNode(parent, x1, y1, x2, y2)
|
||||
--- @type slick.collision.quadTreeNode
|
||||
return self.nodesPool:allocate(self, parent, x1, y1, x2, y2)
|
||||
end
|
||||
|
||||
local _cachedInsertRectangle = rectangle.new()
|
||||
|
||||
--- @overload fun(rectangle: slick.geometry.rectangle)
|
||||
--- @overload fun(p: slick.geometry.rectangle, width: number?, height: number?)
|
||||
--- @overload fun(p1: slick.geometry.point, p2: slick.geometry.point?)
|
||||
--- @overload fun(x1: number?, y1: number?, x2: number?, y2: number?)
|
||||
local function _getRectangle(a, b, c, d)
|
||||
--- @type slick.geometry.rectangle
|
||||
local r
|
||||
if util.is(a, rectangle) then
|
||||
--- @cast a slick.geometry.rectangle
|
||||
r = a
|
||||
elseif util.is(a, point) then
|
||||
r = _cachedInsertRectangle
|
||||
if b and util.is(b, point) then
|
||||
r:init(a.x, a.y, b.x, b.y)
|
||||
else
|
||||
r:init(a.x, a.y, (a.x + (b or 0)), (a.y + (c or 0)))
|
||||
end
|
||||
else
|
||||
r = _cachedInsertRectangle
|
||||
|
||||
--- @cast a number
|
||||
--- @cast b number
|
||||
r:init(a, b, c, d)
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param r slick.geometry.rectangle
|
||||
function quadTree:_tryExpand(r)
|
||||
if r:overlaps(self.bounds) then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.expand then
|
||||
error("shape is completely outside of quad tree and quad tree is not set to auto-expand", 3)
|
||||
return
|
||||
end
|
||||
|
||||
while not r:overlaps(self.bounds) do
|
||||
self.maxLevels = self.maxLevels + 1
|
||||
self.root = self.root:expand(r)
|
||||
|
||||
self.bounds:init(self.root:left(), self.root:top(), self.root:right(), self.root:bottom())
|
||||
end
|
||||
end
|
||||
|
||||
--- Inserts `data` into the tree using the provided bounds.
|
||||
---
|
||||
--- `data` must **not** be in the tree already; if it is, this method will raise an error.
|
||||
--- Instead, use `quadTree.update` to move (or insert) an object.
|
||||
--- @param data any
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, rectangle: slick.geometry.rectangle)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, p: slick.geometry.rectangle, width: number?, height: number?)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, p1: slick.geometry.point, p2: slick.geometry.point?)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, x1: number?, y1: number?, x2: number?, y2: number?)
|
||||
--- @return slick.collision.quadTree
|
||||
function quadTree:insert(data, a, b, c, d)
|
||||
assert(not self.data[data], "data needs to be removed before inserting or use update")
|
||||
|
||||
local r = _getRectangle(a, b, c, d)
|
||||
self:_tryExpand(r)
|
||||
|
||||
self.data[data] = self:_newRectangle(r:left(), r:top(), r:right(), r:bottom())
|
||||
self.root:insert(data, r)
|
||||
end
|
||||
|
||||
--- Removes `data` from the tree.
|
||||
---
|
||||
--- `data` **must** be in the tree already; if it is not, this method will raise an assert.
|
||||
--- @param data any
|
||||
function quadTree:remove(data)
|
||||
assert(self.data[data] ~= nil)
|
||||
|
||||
local r = self.data[data]
|
||||
self.data[data] = nil
|
||||
|
||||
self.root:remove(data, r)
|
||||
self.rectanglesPool:deallocate(r)
|
||||
end
|
||||
|
||||
--- Updates `data` with new bounds.
|
||||
---
|
||||
--- This essentially safely does a `remove` then `insert`.
|
||||
--- @param data any
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, rectangle: slick.geometry.rectangle)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, p: slick.geometry.rectangle, width: number?, height: number?)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, p1: slick.geometry.point, p2: slick.geometry.point?)
|
||||
--- @overload fun(self: slick.collision.quadTree, data: any, x1: number?, y1: number?, x2: number?, y2: number?)
|
||||
--- @see slick.collision.quadTree.insert
|
||||
--- @see slick.collision.quadTree.remove
|
||||
function quadTree:update(data, a, b, c, d)
|
||||
if self.data[data] then
|
||||
self:remove(data)
|
||||
end
|
||||
|
||||
self:insert(data, a, b, c, d)
|
||||
end
|
||||
|
||||
return quadTree
|
||||
374
game/love_src/lib/slick/collision/quadTreeNode.lua
Normal file
374
game/love_src/lib/slick/collision/quadTreeNode.lua
Normal file
@ -0,0 +1,374 @@
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.collision.quadTreeNode
|
||||
--- @field tree slick.collision.quadTree
|
||||
--- @field level number
|
||||
--- @field count number
|
||||
--- @field parent slick.collision.quadTreeNode?
|
||||
--- @field children slick.collision.quadTreeNode[]
|
||||
--- @field data any[]
|
||||
--- @field private uniqueData table<any, boolean>
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
local quadTreeNode = {}
|
||||
local metatable = { __index = quadTreeNode }
|
||||
|
||||
--- @return slick.collision.quadTreeNode
|
||||
function quadTreeNode.new()
|
||||
return setmetatable({
|
||||
level = 0,
|
||||
bounds = rectangle.new(),
|
||||
count = 0,
|
||||
children = {},
|
||||
data = {},
|
||||
uniqueData = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param tree slick.collision.quadTree
|
||||
--- @param parent slick.collision.quadTreeNode?
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
function quadTreeNode:init(tree, parent, x1, y1, x2, y2)
|
||||
self.tree = tree
|
||||
self.parent = parent
|
||||
self.level = (parent and parent.level or 0) + 1
|
||||
self.depth = self.level
|
||||
self.bounds:init(x1, y1, x2, y2)
|
||||
|
||||
slicktable.clear(self.children)
|
||||
slicktable.clear(self.data)
|
||||
slicktable.clear(self.uniqueData)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param parent slick.collision.quadTreeNode?
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @return slick.collision.quadTreeNode
|
||||
function quadTreeNode:_newNode(parent, x1, y1, x2, y2)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
return self.tree:_newNode(parent, x1, y1, x2, y2)
|
||||
end
|
||||
|
||||
--- Returns the maximum left of the quad tree.
|
||||
--- @return number
|
||||
function quadTreeNode:left()
|
||||
return self.bounds:left()
|
||||
end
|
||||
|
||||
--- Returns the maximum right of the quad tree.
|
||||
--- @return number
|
||||
function quadTreeNode:right()
|
||||
return self.bounds:right()
|
||||
end
|
||||
|
||||
--- Returns the maximum top of the quad tree.
|
||||
--- @return number
|
||||
function quadTreeNode:top()
|
||||
return self.bounds:top()
|
||||
end
|
||||
|
||||
--- Returns the maximum bottom of the quad tree.
|
||||
--- @return number
|
||||
function quadTreeNode:bottom()
|
||||
return self.bounds:bottom()
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function quadTreeNode:width()
|
||||
return self.bounds:width()
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function quadTreeNode:height()
|
||||
return self.bounds:height()
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode
|
||||
function quadTreeNode._incrementLevel(node)
|
||||
node.level = node.level + 1
|
||||
end
|
||||
|
||||
--- Visits all nodes, including this one, calling `func` on the node.
|
||||
--- @param func fun(node: slick.collision.quadTreeNode)
|
||||
function quadTreeNode:visit(func)
|
||||
func(self)
|
||||
|
||||
for _, c in ipairs(self.children) do
|
||||
c:visit(func)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param func fun(node: slick.collision.quadTreeNode)
|
||||
--- @param ignore slick.collision.quadTreeNode
|
||||
function quadTreeNode:_visit(func, ignore)
|
||||
if self == ignore then
|
||||
return
|
||||
end
|
||||
|
||||
func(self)
|
||||
|
||||
for _, c in ipairs(self.children) do
|
||||
c:visit(func)
|
||||
end
|
||||
end
|
||||
|
||||
--- Visits all parent nodes and their children, calling `func` on the node.
|
||||
--- This method iterates from the parent node down, skipping the node `ascend` was called on.
|
||||
--- @param func fun(node: slick.collision.quadTreeNode)
|
||||
function quadTreeNode:ascend(func)
|
||||
if self.tree.root ~= self then
|
||||
self.tree.root:_visit(func, self)
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedQuadTreeNodeData = {}
|
||||
|
||||
--- @param node slick.collision.quadTreeNode
|
||||
local function _gatherData(node)
|
||||
for _, data in ipairs(node.data) do
|
||||
_cachedQuadTreeNodeData[data] = true
|
||||
end
|
||||
end
|
||||
|
||||
--- Expands this node to fit 'bounds'.
|
||||
--- @param bounds slick.geometry.rectangle
|
||||
--- @return slick.collision.quadTreeNode
|
||||
function quadTreeNode:expand(bounds)
|
||||
assert(not self.parent, "can only expand root node")
|
||||
assert(not bounds:overlaps(self.bounds), "bounds is within quad tree")
|
||||
assert(bounds:left() > -math.huge and bounds:right() < math.huge, "x axis infinite")
|
||||
assert(bounds:top() > -math.huge and bounds:bottom() < math.huge, "y axis infinite")
|
||||
|
||||
slicktable.clear(_cachedQuadTreeNodeData)
|
||||
self:visit(_gatherData)
|
||||
|
||||
local halfWidth = self:width() / 2
|
||||
local halfHeight = self:height() / 2
|
||||
|
||||
local left, right = false, false
|
||||
if bounds:right() < self:left() + halfWidth then
|
||||
right = true
|
||||
else
|
||||
left = true
|
||||
end
|
||||
|
||||
local top, bottom = false, false
|
||||
if bounds:bottom() < self:top() + halfHeight then
|
||||
bottom = true
|
||||
else
|
||||
top = true
|
||||
end
|
||||
|
||||
local parent
|
||||
local topLeft, topRight, bottomLeft, bottomRight
|
||||
if top and left then
|
||||
parent = self:_newNode(nil, self:left(), self:top(), self:right() + self:width(), self:bottom() + self:height())
|
||||
topLeft = self
|
||||
topRight = self:_newNode(parent, self:right(), self:top(), self:right() + self:width(), self:bottom())
|
||||
bottomLeft = self:_newNode(parent, self:left(), self:bottom(), self:right(), self:bottom() + self:height())
|
||||
bottomRight = self:_newNode(parent, self:right(), self:bottom(), self:right() + self:width(), self:bottom() + self:height())
|
||||
elseif top and right then
|
||||
parent = self:_newNode(nil, self:left() - self:width(), self:top(), self:right(), self:bottom() + self:height())
|
||||
topLeft = self:_newNode(parent, self:left() - self:width(), self:top(), self:left(), self:bottom())
|
||||
topRight = self
|
||||
bottomLeft = self:_newNode(parent, self:left() - self:width(), self:bottom(), self:left(), self:bottom() + self:height())
|
||||
bottomRight = self:_newNode(parent, self:left(), self:bottom(), self:right(), self:bottom() + self:height())
|
||||
elseif bottom and left then
|
||||
parent = self:_newNode(nil, self:left(), self:top() - self:height(), self:right() + self:width(), self:bottom())
|
||||
topLeft = self:_newNode(parent, self:left(), self:top() - self:height(), self:right(), self:top())
|
||||
topRight = self:_newNode(parent, self:right(), self:top() - self:height(), self:right() + self:width(), self:top())
|
||||
bottomLeft = self
|
||||
bottomRight = self:_newNode(parent, self:right(), self:top(), self:right() + self:width(), self:bottom())
|
||||
elseif bottom and right then
|
||||
parent = self:_newNode(nil, self:left() - self:width(), self:top() - self:height(), self:right(), self:bottom())
|
||||
topLeft = self:_newNode(parent, self:left() - self:width(), self:top() - self:height(), self:left(), self:top())
|
||||
topRight = self:_newNode(parent, self:left(), self:top() - self:height(), self:right(), self:top())
|
||||
bottomLeft = self:_newNode(parent, self:left() - self:width(), self:top(), self:left(), self:bottom())
|
||||
bottomRight = self
|
||||
else
|
||||
assert(false, "critical logic error")
|
||||
end
|
||||
|
||||
table.insert(parent.children, topLeft)
|
||||
table.insert(parent.children, topRight)
|
||||
table.insert(parent.children, bottomLeft)
|
||||
table.insert(parent.children, bottomRight)
|
||||
|
||||
for _, child in ipairs(parent.children) do
|
||||
if child ~= self then
|
||||
for data in pairs(_cachedQuadTreeNodeData) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local r = self.tree.data[data]
|
||||
if r:overlaps(child.bounds) then
|
||||
child:insert(data, r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:visit(quadTreeNode._incrementLevel)
|
||||
|
||||
parent.count = self.count
|
||||
self.parent = parent
|
||||
|
||||
return parent
|
||||
end
|
||||
|
||||
--- Inserts `data` given the `bounds` into this node.
|
||||
---
|
||||
--- `data` must not already be added to this node.
|
||||
--- @param data any
|
||||
--- @param bounds slick.geometry.rectangle
|
||||
function quadTreeNode:insert(data, bounds)
|
||||
if (#self.children == 0 and #self.data < self.tree.maxData) or self.level >= self.tree.maxLevels then
|
||||
assert(self.uniqueData[data] == nil, "data is already in node")
|
||||
|
||||
self.uniqueData[data] = true
|
||||
table.insert(self.data, data)
|
||||
|
||||
self.count = self.count + 1
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if #self.children == 0 and #self.data >= self.tree.maxData then
|
||||
self.count = 0
|
||||
self:split()
|
||||
end
|
||||
|
||||
for _, child in ipairs(self.children) do
|
||||
if bounds:overlaps(child.bounds) then
|
||||
child:insert(data, bounds)
|
||||
end
|
||||
end
|
||||
|
||||
self.count = self.count + 1
|
||||
end
|
||||
|
||||
--- @param data any
|
||||
--- @param bounds slick.geometry.rectangle
|
||||
function quadTreeNode:remove(data, bounds)
|
||||
if #self.children > 0 then
|
||||
for _, child in ipairs(self.children) do
|
||||
if bounds:overlaps(child.bounds) then
|
||||
child:remove(data, bounds)
|
||||
end
|
||||
end
|
||||
|
||||
self.count = self.count - 1
|
||||
|
||||
if self.count <= self.tree.maxData then
|
||||
self:collapse()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if not self.uniqueData[data] then
|
||||
return
|
||||
end
|
||||
|
||||
for i, d in ipairs(self.data) do
|
||||
if d == data then
|
||||
table.remove(self.data, i)
|
||||
self.count = self.count - 1
|
||||
|
||||
self.uniqueData[d] = nil
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Splits the node into children nodes.
|
||||
--- Moves any data from this node to the children nodes.
|
||||
function quadTreeNode:split()
|
||||
assert(#self.data >= self.tree.maxData, "cannot split; still has room")
|
||||
|
||||
local width = self:right() - self:left()
|
||||
local height = self:bottom() - self:top()
|
||||
|
||||
local childWidth = width / 2
|
||||
local childHeight = height / 2
|
||||
|
||||
local topLeft = self:_newNode(self, self:left(), self:top(), self:left() + childWidth, self:top() + childHeight)
|
||||
local topRight = self:_newNode(self, self:left() + childWidth, self:top(), self:right(), self:top() + childHeight)
|
||||
local bottomLeft = self:_newNode(self, self:left(), self:top() + childHeight, self:left() + childWidth, self:bottom())
|
||||
local bottomRight = self:_newNode(self, self:left() + childWidth, self:top() + childHeight, self:right(), self:bottom())
|
||||
|
||||
table.insert(self.children, topLeft)
|
||||
table.insert(self.children, topRight)
|
||||
table.insert(self.children, bottomLeft)
|
||||
table.insert(self.children, bottomRight)
|
||||
|
||||
for _, data in ipairs(self.data) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local r = self.tree.data[data]
|
||||
self:insert(data, r)
|
||||
end
|
||||
|
||||
slicktable.clear(self.data)
|
||||
slicktable.clear(self.uniqueData)
|
||||
end
|
||||
|
||||
local _collectResult = { n = 0, unique = {}, data = {} }
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode
|
||||
function quadTreeNode._collect(node)
|
||||
for _, data in ipairs(node.data) do
|
||||
if not _collectResult.unique[data] then
|
||||
_collectResult.unique[data] = true
|
||||
table.insert(_collectResult.data, data)
|
||||
|
||||
_collectResult.n = _collectResult.n + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @package
|
||||
--- Deallocates all children nodes.
|
||||
function quadTreeNode:_snip()
|
||||
for _, child in ipairs(self.children) do
|
||||
child:_snip()
|
||||
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self.tree.nodesPool:deallocate(child)
|
||||
end
|
||||
|
||||
slicktable.clear(self.children)
|
||||
end
|
||||
|
||||
--- Collapses the children node into this node.
|
||||
function quadTreeNode:collapse()
|
||||
_collectResult.n = 0
|
||||
slicktable.clear(_collectResult.unique)
|
||||
slicktable.clear(_collectResult.data)
|
||||
|
||||
self:visit(self._collect)
|
||||
|
||||
if _collectResult.n <= self.tree.maxData then
|
||||
self:_snip()
|
||||
|
||||
for _, data in ipairs(_collectResult.data) do
|
||||
self.uniqueData[data] = true
|
||||
table.insert(self.data, data)
|
||||
end
|
||||
|
||||
self.count = #self.data
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return quadTreeNode
|
||||
232
game/love_src/lib/slick/collision/quadTreeQuery.lua
Normal file
232
game/love_src/lib/slick/collision/quadTreeQuery.lua
Normal file
@ -0,0 +1,232 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local ray = require("slick.geometry.ray")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local util = require("slick.util")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.collision.quadTreeQuery
|
||||
--- @field tree slick.collision.quadTree
|
||||
--- @field results any[]
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field private data table<any, boolean>
|
||||
local quadTreeQuery = {}
|
||||
local metatable = { __index = quadTreeQuery }
|
||||
|
||||
--- @param tree slick.collision.quadTree
|
||||
--- @return slick.collision.quadTreeQuery
|
||||
function quadTreeQuery.new(tree)
|
||||
return setmetatable({
|
||||
tree = tree,
|
||||
results = {},
|
||||
bounds = rectangle.new(),
|
||||
data = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function quadTreeQuery:_beginQuery()
|
||||
slicktable.clear(self.results)
|
||||
slicktable.clear(self.data)
|
||||
|
||||
self.bounds.topLeft:init(math.huge, math.huge)
|
||||
self.bounds.bottomRight:init(-math.huge, -math.huge)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param r slick.geometry.rectangle
|
||||
function quadTreeQuery:_expand(r)
|
||||
self.bounds.topLeft.x = math.min(self.bounds.topLeft.x, r.topLeft.x)
|
||||
self.bounds.topLeft.y = math.min(self.bounds.topLeft.y, r.topLeft.y)
|
||||
self.bounds.bottomRight.x = math.max(self.bounds.bottomRight.x, r.bottomRight.x)
|
||||
self.bounds.bottomRight.y = math.max(self.bounds.bottomRight.y, r.bottomRight.y)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function quadTreeQuery:_endQuery()
|
||||
if #self.results == 0 then
|
||||
self.bounds:init(0, 0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode?
|
||||
--- @param p slick.geometry.point
|
||||
--- @param E number
|
||||
function quadTreeQuery:_performPointQuery(node, p, E)
|
||||
if not node then
|
||||
node = self.tree.root
|
||||
self:_beginQuery()
|
||||
end
|
||||
|
||||
if slickmath.withinRange(p.x, node:left(), node:right(), E) and slickmath.withinRange(p.y, node:top(), node:bottom(), E) then
|
||||
if #node.children > 0 then
|
||||
for _, c in ipairs(node.children) do
|
||||
self:_performPointQuery(c, p, E)
|
||||
end
|
||||
else
|
||||
for _, d in ipairs(node.data) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local r = self.tree.data[d]
|
||||
if not self.data[d] and slickmath.withinRange(p.x, r:left(), r:right(), E) and slickmath.withinRange(p.y, r:top(), r:bottom(), E) then
|
||||
table.insert(self.results, d)
|
||||
self.data[d] = true
|
||||
self:_expand(r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode?
|
||||
--- @param r slick.geometry.rectangle
|
||||
function quadTreeQuery:_performRectangleQuery(node, r)
|
||||
if not node then
|
||||
node = self.tree.root
|
||||
self:_beginQuery()
|
||||
end
|
||||
|
||||
if r:overlaps(node.bounds) then
|
||||
if #node.children > 0 then
|
||||
for _, c in ipairs(node.children) do
|
||||
self:_performRectangleQuery(c, r)
|
||||
end
|
||||
else
|
||||
for _, d in ipairs(node.data) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local otherRectangle = self.tree.data[d]
|
||||
|
||||
if not self.data[d] and r:overlaps(otherRectangle) then
|
||||
table.insert(self.results, d)
|
||||
self.data[d] = true
|
||||
|
||||
self:_expand(r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedQuerySegment = segment.new()
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode?
|
||||
--- @param s slick.geometry.segment
|
||||
--- @param E number
|
||||
function quadTreeQuery:_performSegmentQuery(node, s, E)
|
||||
if not node then
|
||||
node = self.tree.root
|
||||
self:_beginQuery()
|
||||
end
|
||||
|
||||
local overlaps = (s:left() <= node:right() + E and s:right() + E >= node:left()) and
|
||||
(s:top() <= node:bottom() + E and s:bottom() + E >= node:top())
|
||||
if overlaps then
|
||||
if #node.children > 0 then
|
||||
for _, c in ipairs(node.children) do
|
||||
self:_performSegmentQuery(c, s, E)
|
||||
end
|
||||
else
|
||||
for _, d in ipairs(node.data) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local r = self.tree.data[d]
|
||||
|
||||
local intersection
|
||||
|
||||
-- Top
|
||||
_cachedQuerySegment.a:init(r:left(), r:top())
|
||||
_cachedQuerySegment.b:init(r:right(), r:top())
|
||||
intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E)
|
||||
|
||||
if not intersection then
|
||||
-- Right
|
||||
_cachedQuerySegment.a:init(r:right(), r:top())
|
||||
_cachedQuerySegment.b:init(r:right(), r:bottom())
|
||||
intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E)
|
||||
end
|
||||
|
||||
if not intersection then
|
||||
-- Bottom
|
||||
_cachedQuerySegment.a:init(r:right(), r:bottom())
|
||||
_cachedQuerySegment.b:init(r:left(), r:bottom())
|
||||
intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E)
|
||||
end
|
||||
|
||||
if not intersection then
|
||||
-- Left
|
||||
_cachedQuerySegment.a:init(r:left(), r:bottom())
|
||||
_cachedQuerySegment.b:init(r:left(), r:top())
|
||||
intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E)
|
||||
end
|
||||
|
||||
if intersection or (r:inside(s.a) or r:inside(s.b)) then
|
||||
table.insert(self.results, d)
|
||||
self.data[d] = true
|
||||
|
||||
self:_expand(r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param node slick.collision.quadTreeNode?
|
||||
--- @param r slick.geometry.ray
|
||||
function quadTreeQuery:_performRayQuery(node, r)
|
||||
if not node then
|
||||
node = self.tree.root
|
||||
self:_beginQuery()
|
||||
end
|
||||
|
||||
if r:hitRectangle(node.bounds) then
|
||||
if #node.children > 0 then
|
||||
for _, c in ipairs(node.children) do
|
||||
self:_performRayQuery(c, r)
|
||||
end
|
||||
else
|
||||
for _, d in ipairs(node.data) do
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
local bounds = self.tree.data[d]
|
||||
|
||||
if r:hitRectangle(bounds) then
|
||||
table.insert(self.results, d)
|
||||
self.data[d] = true
|
||||
|
||||
self:_expand(bounds)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Performs a query against the quad tree with the provided shape.
|
||||
--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray
|
||||
--- @param E number?
|
||||
function quadTreeQuery:perform(shape, E)
|
||||
E = E or 0
|
||||
|
||||
if util.is(shape, point) then
|
||||
--- @cast shape slick.geometry.point
|
||||
self:_performPointQuery(nil, shape, E)
|
||||
elseif util.is(shape, rectangle) then
|
||||
--- @cast shape slick.geometry.rectangle
|
||||
self:_performRectangleQuery(nil, shape)
|
||||
elseif util.is(shape, segment) then
|
||||
--- @cast shape slick.geometry.segment
|
||||
self:_performSegmentQuery(nil, shape, E)
|
||||
elseif util.is(shape, ray) then
|
||||
--- @cast shape slick.geometry.ray
|
||||
self:_performRayQuery(nil, shape)
|
||||
else
|
||||
error("unhandled shape type in query; expected point, rectangle, segment, or ray", 2)
|
||||
end
|
||||
|
||||
self:_endQuery()
|
||||
|
||||
return #self.results > 0
|
||||
end
|
||||
|
||||
return quadTreeQuery
|
||||
@ -0,0 +1,909 @@
|
||||
local interval = require "slick.collision.interval"
|
||||
local point = require "slick.geometry.point"
|
||||
local segment = require "slick.geometry.segment"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
local slicktable = require "slick.util.slicktable"
|
||||
|
||||
local SIDE_NONE = 0
|
||||
local SIDE_LEFT = -1
|
||||
local SIDE_RIGHT = 1
|
||||
|
||||
--- @alias slick.collision.shapeCollisionResolutionQueryAxis {
|
||||
--- parent: slick.collision.shapeCollisionResolutionQueryShape,
|
||||
--- normal: slick.geometry.point,
|
||||
--- segment: slick.geometry.segment,
|
||||
--- }
|
||||
|
||||
--- @alias slick.collision.shapeCollisionResolutionQueryShape {
|
||||
--- shape: slick.collision.shapeInterface,
|
||||
--- offset: slick.geometry.point,
|
||||
--- axesCount: number,
|
||||
--- axes: slick.collision.shapeCollisionResolutionQueryAxis[],
|
||||
--- currentInterval: slick.collision.interval,
|
||||
--- minInterval: slick.collision.interval,
|
||||
--- }
|
||||
|
||||
--- @class slick.collision.shapeCollisionResolutionQuery
|
||||
--- @field epsilon number
|
||||
--- @field collision boolean
|
||||
--- @field normal slick.geometry.point
|
||||
--- @field currentNormal slick.geometry.point
|
||||
--- @field otherNormal slick.geometry.point
|
||||
--- @field depth number
|
||||
--- @field private otherDepth number
|
||||
--- @field currentDepth number
|
||||
--- @field time number
|
||||
--- @field currentOffset slick.geometry.point
|
||||
--- @field otherOffset slick.geometry.point
|
||||
--- @field contactPointsCount number
|
||||
--- @field contactPoints slick.geometry.point[]
|
||||
--- @field normals slick.geometry.point[]
|
||||
--- @field alternateNormals slick.geometry.point[]
|
||||
--- @field private allNormals slick.geometry.point[]
|
||||
--- @field private allNormalsCount number
|
||||
--- @field private depths number[]
|
||||
--- @field private normalsShape slick.collision.shapeCollisionResolutionQueryShape[]
|
||||
--- @field private firstTime number
|
||||
--- @field private lastTime number
|
||||
--- @field private currentShape slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @field private otherShape slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @field private axis slick.collision.shapeCollisionResolutionQueryAxis?
|
||||
--- @field private otherAxis slick.collision.shapeCollisionResolutionQueryAxis?
|
||||
--- @field private currentAxis slick.collision.shapeCollisionResolutionQueryAxis?
|
||||
--- @field private relativeDirection slick.geometry.point
|
||||
local shapeCollisionResolutionQuery = {}
|
||||
local metatable = { __index = shapeCollisionResolutionQuery }
|
||||
|
||||
--- @return slick.collision.shapeCollisionResolutionQueryShape
|
||||
local function _newQueryShape()
|
||||
return {
|
||||
offset = point.new(),
|
||||
axesCount = 0,
|
||||
axes = {},
|
||||
currentInterval = interval.new(),
|
||||
minInterval = interval.new(),
|
||||
}
|
||||
end
|
||||
|
||||
--- @param E number?
|
||||
--- @return slick.collision.shapeCollisionResolutionQuery
|
||||
function shapeCollisionResolutionQuery.new(E)
|
||||
return setmetatable({
|
||||
epsilon = E or slickmath.EPSILON,
|
||||
collision = false,
|
||||
depth = 0,
|
||||
currentDepth = 0,
|
||||
otherDepth = 0,
|
||||
normal = point.new(),
|
||||
currentNormal = point.new(),
|
||||
otherNormal = point.new(),
|
||||
time = 0,
|
||||
firstTime = 0,
|
||||
lastTime = 0,
|
||||
currentOffset = point.new(),
|
||||
otherOffset = point.new(),
|
||||
contactPointsCount = 0,
|
||||
contactPoints = { point.new() },
|
||||
normals = {},
|
||||
alternateNormals = {},
|
||||
depths = {},
|
||||
normalsShape = {},
|
||||
allNormals = {},
|
||||
allNormalsCount = 0,
|
||||
currentShape = _newQueryShape(),
|
||||
otherShape = _newQueryShape(),
|
||||
relativeDirection = point.new()
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @return slick.collision.shapeInterface
|
||||
function shapeCollisionResolutionQuery:getSelfShape()
|
||||
return self.currentShape.shape
|
||||
end
|
||||
|
||||
--- @return slick.collision.shapeInterface
|
||||
function shapeCollisionResolutionQuery:getOtherShape()
|
||||
return self.otherShape.shape
|
||||
end
|
||||
|
||||
--- @private
|
||||
function shapeCollisionResolutionQuery:_swapShapes()
|
||||
self.otherShape, self.currentShape = self.currentShape, self.otherShape
|
||||
end
|
||||
|
||||
function shapeCollisionResolutionQuery:reset()
|
||||
self.collision = false
|
||||
self.depth = 0
|
||||
self.otherDepth = 0
|
||||
self.currentDepth = 0
|
||||
self.time = math.huge
|
||||
self.currentOffset:init(0, 0)
|
||||
self.otherOffset:init(0, 0)
|
||||
self.normal:init(0, 0)
|
||||
self.otherNormal:init(0, 0)
|
||||
self.currentNormal:init(0, 0)
|
||||
self.contactPointsCount = 0
|
||||
self.allNormalsCount = 0
|
||||
|
||||
slicktable.clear(self.normals)
|
||||
slicktable.clear(self.alternateNormals)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function shapeCollisionResolutionQuery:_beginQuery()
|
||||
self.currentShape.axesCount = 0
|
||||
self.otherShape.axesCount = 0
|
||||
self.axis = nil
|
||||
self.otherAxis = nil
|
||||
self.currentAxis = nil
|
||||
|
||||
self.collision = false
|
||||
self.depth = 0
|
||||
self.otherDepth = 0
|
||||
self.currentDepth = 0
|
||||
self.firstTime = -math.huge
|
||||
self.lastTime = math.huge
|
||||
self.currentOffset:init(0, 0)
|
||||
self.otherOffset:init(0, 0)
|
||||
self.normal:init(0, 0)
|
||||
self.otherNormal:init(0, 0)
|
||||
self.currentNormal:init(0, 0)
|
||||
self.contactPointsCount = 0
|
||||
self.relativeDirection:init(0, 0)
|
||||
self.allNormalsCount = 0
|
||||
end
|
||||
|
||||
function shapeCollisionResolutionQuery:addAxis()
|
||||
self.currentShape.axesCount = self.currentShape.axesCount + 1
|
||||
local index = self.currentShape.axesCount
|
||||
local axis = self.currentShape.axes[index]
|
||||
if not axis then
|
||||
axis = { parent = self.currentShape, normal = point.new(), segment = segment.new() }
|
||||
self.currentShape.axes[index] = axis
|
||||
end
|
||||
|
||||
return axis
|
||||
end
|
||||
|
||||
local _cachedOtherSegment = segment.new()
|
||||
local _cachedCurrentPoint = point.new()
|
||||
local _cachedOtherNormal = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param a slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @param b slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @param aOffset slick.geometry.point
|
||||
--- @param bOffset slick.geometry.point
|
||||
--- @param scalar number
|
||||
--- @return boolean
|
||||
function shapeCollisionResolutionQuery:_isShapeMovingAwayFromShape(a, b, aOffset, bOffset, scalar)
|
||||
local currentVertexCount = a.shape.vertexCount
|
||||
local currentVertices = a.shape.vertices
|
||||
|
||||
local otherVertexCount = b.shape.vertexCount
|
||||
local otherVertices = b.shape.vertices
|
||||
|
||||
for i = 1, otherVertexCount do
|
||||
local j = slickmath.wrap(i, 1, otherVertexCount)
|
||||
|
||||
otherVertices[i]:add(bOffset, _cachedOtherSegment.a)
|
||||
otherVertices[j]:add(bOffset, _cachedOtherSegment.b)
|
||||
|
||||
_cachedOtherSegment.a:direction(_cachedOtherSegment.b, _cachedOtherNormal)
|
||||
_cachedOtherNormal:normalize(_cachedOtherNormal)
|
||||
_cachedOtherNormal:left(_cachedOtherNormal)
|
||||
|
||||
local sameSide = true
|
||||
for k = 1, currentVertexCount do
|
||||
currentVertices[k]:add(aOffset, _cachedCurrentPoint)
|
||||
|
||||
local direction = slickmath.direction(_cachedOtherSegment.a, _cachedOtherSegment.b, _cachedCurrentPoint, self.epsilon)
|
||||
if direction < 0 then
|
||||
sameSide = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if sameSide then
|
||||
if (scalar * self.relativeDirection:dot(_cachedOtherNormal)) >= -self.epsilon then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local _lineSegmentDirection = point.new()
|
||||
local _lineSegmentRelativePosition = point.new()
|
||||
local _lineSegmentShapePosition = point.new()
|
||||
local _lineSegmentShapeVertexPosition = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param shape slick.collision.shapeInterface
|
||||
--- @param offset slick.geometry.point
|
||||
--- @param direction slick.geometry.point
|
||||
--- @param point slick.geometry.point
|
||||
--- @param fun fun(number, number): number
|
||||
--- @return number
|
||||
function shapeCollisionResolutionQuery:_dotShapeSegment(shape, offset, direction, point, fun)
|
||||
offset:sub(point, _lineSegmentRelativePosition)
|
||||
|
||||
local dot
|
||||
for i = 1, shape.vertexCount do
|
||||
local vertex = shape.vertices[i]
|
||||
vertex:add(_lineSegmentRelativePosition, _lineSegmentShapeVertexPosition)
|
||||
local d = direction:dot(_lineSegmentShapeVertexPosition)
|
||||
dot = fun(d, dot or d)
|
||||
end
|
||||
|
||||
return dot
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param lineSegmentShape slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @param otherShape slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @param otherOffset slick.geometry.point
|
||||
--- @param worldOffset slick.geometry.point
|
||||
function shapeCollisionResolutionQuery:_correctLineSegmentNormals(lineSegmentShape, otherShape, otherOffset, worldOffset)
|
||||
assert(lineSegmentShape.shape.vertexCount == 2, "shape must be line segment")
|
||||
|
||||
worldOffset:add(otherOffset, _lineSegmentShapePosition)
|
||||
|
||||
local a = lineSegmentShape.shape.vertices[1]
|
||||
local b = lineSegmentShape.shape.vertices[2]
|
||||
a:direction(b, _lineSegmentDirection)
|
||||
|
||||
-- Check if we're behind a (the beginning of the segment) or in front of b (the end of the segment)
|
||||
local dotA = self:_dotShapeSegment(otherShape.shape, _lineSegmentShapePosition, _lineSegmentDirection, a, math.max)
|
||||
local dotB = self:_dotShapeSegment(otherShape.shape, _lineSegmentShapePosition, _lineSegmentDirection, b, math.min)
|
||||
|
||||
local normal, depth
|
||||
if lineSegmentShape == self.currentShape then
|
||||
normal = self.currentNormal
|
||||
depth = self.currentDepth
|
||||
elseif lineSegmentShape == self.otherShape then
|
||||
normal = self.otherNormal
|
||||
depth = self.otherDepth
|
||||
end
|
||||
assert(normal and depth, "incorrect shape; couldn't determine normal")
|
||||
|
||||
if not (dotA > 0 and dotB < 0) then
|
||||
-- If we're not to the side of the segment, we need to swap the normal.
|
||||
self:_addNormal(depth, lineSegmentShape, normal.x, normal.y)
|
||||
normal:left(normal)
|
||||
|
||||
if dotA >= 0 and dotB >= 0 then
|
||||
normal:negate(normal)
|
||||
end
|
||||
else
|
||||
otherShape.shape.center:add(_lineSegmentShapePosition, _lineSegmentShapePosition)
|
||||
local side = slickmath.direction(
|
||||
lineSegmentShape.shape.vertices[1],
|
||||
lineSegmentShape.shape.vertices[2],
|
||||
_lineSegmentShapePosition)
|
||||
|
||||
if side == 0 then
|
||||
self:_addNormal(depth, lineSegmentShape, -normal.x, -normal.y)
|
||||
else
|
||||
normal:multiplyScalar(side, normal)
|
||||
end
|
||||
end
|
||||
|
||||
normal:negate(normal)
|
||||
if normal == self.otherNormal then
|
||||
self.normal:init(normal.x, normal.y)
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedRelativeVelocity = point.new()
|
||||
local _cachedSelfFutureCenter = point.new()
|
||||
local _cachedSelfVelocityMinusOffset = point.new()
|
||||
local _cachedDirection = point.new()
|
||||
|
||||
local _cachedSegmentA = segment.new()
|
||||
local _cachedSegmentB = segment.new()
|
||||
|
||||
--- @private
|
||||
--- @param selfShape slick.collision.commonShape
|
||||
--- @param otherShape slick.collision.commonShape
|
||||
--- @param selfOffset slick.geometry.point
|
||||
--- @param otherOffset slick.geometry.point
|
||||
--- @param selfVelocity slick.geometry.point
|
||||
--- @param otherVelocity slick.geometry.point
|
||||
function shapeCollisionResolutionQuery:_performPolygonPolygonProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity)
|
||||
self.currentShape.shape = selfShape
|
||||
self.currentShape.offset:init(selfOffset.x, selfOffset.y)
|
||||
self.otherShape.shape = otherShape
|
||||
self.otherShape.offset:init(otherOffset.x, otherOffset.y)
|
||||
|
||||
self.currentShape.shape:getAxes(self)
|
||||
self:_swapShapes()
|
||||
self.currentShape.shape:getAxes(self)
|
||||
self:_swapShapes()
|
||||
|
||||
otherVelocity:sub(selfVelocity, _cachedRelativeVelocity)
|
||||
selfVelocity:add(selfShape.center, _cachedSelfFutureCenter)
|
||||
|
||||
selfVelocity:sub(selfOffset, _cachedSelfVelocityMinusOffset)
|
||||
|
||||
_cachedRelativeVelocity:normalize(self.relativeDirection)
|
||||
self.relativeDirection:negate(self.relativeDirection)
|
||||
|
||||
self.depth = math.huge
|
||||
self.otherDepth = math.huge
|
||||
self.currentDepth = math.huge
|
||||
|
||||
local hit = true
|
||||
local side = SIDE_NONE
|
||||
|
||||
local currentInterval = self.currentShape.currentInterval
|
||||
local otherInterval = self.otherShape.currentInterval
|
||||
|
||||
local isTouching = true
|
||||
if _cachedRelativeVelocity:lengthSquared() == 0 then
|
||||
for i = 1, self.currentShape.axesCount + self.otherShape.axesCount do
|
||||
local axis = self:_getAxis(i)
|
||||
|
||||
currentInterval:init()
|
||||
otherInterval:init()
|
||||
|
||||
self:_handleAxis(axis)
|
||||
|
||||
if self:_compareIntervals(axis) then
|
||||
hit = true
|
||||
else
|
||||
hit = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hit then
|
||||
isTouching = false
|
||||
end
|
||||
else
|
||||
for i = 1, self.currentShape.axesCount + self.otherShape.axesCount do
|
||||
local axis = self:_getAxis(i)
|
||||
|
||||
currentInterval:init()
|
||||
otherInterval:init()
|
||||
|
||||
local willHit, futureSide = self:_handleTunnelAxis(axis, _cachedRelativeVelocity)
|
||||
if not willHit then
|
||||
hit = false
|
||||
|
||||
if not isTouching then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if isTouching and not self:_compareIntervals(axis) then
|
||||
isTouching = false
|
||||
|
||||
if not hit then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if futureSide then
|
||||
currentInterval:copy(self.currentShape.minInterval)
|
||||
otherInterval:copy(self.otherShape.minInterval)
|
||||
|
||||
side = futureSide
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if hit and (self.depth == math.huge or self.depth < self.epsilon) and _cachedRelativeVelocity:lengthSquared() > 0 then
|
||||
hit = not (
|
||||
self:_isShapeMovingAwayFromShape(self.currentShape, self.otherShape, selfOffset, otherOffset, 1) or
|
||||
self:_isShapeMovingAwayFromShape(self.otherShape, self.currentShape, otherOffset, selfOffset, -1))
|
||||
end
|
||||
|
||||
if self.firstTime > 1 then
|
||||
hit = false
|
||||
end
|
||||
|
||||
if not hit and self.depth < self.epsilon then
|
||||
self.depth = 0
|
||||
end
|
||||
|
||||
if self.firstTime == -math.huge and self.lastTime >= 0 and self.lastTime <= 1 then
|
||||
self.firstTime = 0
|
||||
end
|
||||
|
||||
if hit and isTouching then
|
||||
self.currentShape.shape.center:direction(self.otherShape.shape.center, _cachedDirection)
|
||||
_cachedDirection:normalize(_cachedDirection)
|
||||
|
||||
if _cachedDirection:dot(self.normal) > 0 then
|
||||
self.normal:negate(self.normal)
|
||||
end
|
||||
|
||||
if _cachedDirection:dot(self.currentNormal) < 0 then
|
||||
self.currentNormal:negate(self.currentNormal)
|
||||
end
|
||||
end
|
||||
|
||||
if self.firstTime <= 0 and self.depth == 0 and _cachedRelativeVelocity:lengthSquared() == 0 then
|
||||
hit = false
|
||||
end
|
||||
|
||||
if not hit then
|
||||
self:_clear()
|
||||
return
|
||||
end
|
||||
|
||||
self.time = math.max(self.firstTime, 0)
|
||||
|
||||
if (self.firstTime == 0 and self.lastTime <= 1) or (self.firstTime == -math.huge and self.lastTime == math.huge) then
|
||||
self.normal:multiplyScalar(self.depth, self.currentOffset)
|
||||
self.normal:multiplyScalar(-self.depth, self.otherOffset)
|
||||
else
|
||||
selfVelocity:multiplyScalar(self.time, self.currentOffset)
|
||||
otherVelocity:multiplyScalar(self.time, self.otherOffset)
|
||||
end
|
||||
|
||||
if self.time > 0 and self.currentOffset:lengthSquared() == 0 then
|
||||
self.time = 0
|
||||
self.depth = 0
|
||||
end
|
||||
|
||||
if side == SIDE_RIGHT or side == SIDE_LEFT then
|
||||
if not isTouching then
|
||||
self.allNormalsCount = 0
|
||||
end
|
||||
|
||||
local currentInterval = self.currentShape.minInterval
|
||||
local otherInterval = self.otherShape.minInterval
|
||||
|
||||
currentInterval:sort()
|
||||
otherInterval:sort()
|
||||
|
||||
if side == SIDE_LEFT then
|
||||
local selfA = currentInterval.indices[currentInterval.minIndex].index
|
||||
local selfB = currentInterval.indices[currentInterval.minIndex + 1].index
|
||||
if ((selfA == 1 or selfB == 1) and (selfA == selfShape.vertexCount or selfB == selfShape.vertexCount)) then
|
||||
selfA, selfB = math.max(selfA, selfB), math.min(selfA, selfB)
|
||||
else
|
||||
selfA, selfB = math.min(selfA, selfB), math.max(selfA, selfB)
|
||||
end
|
||||
|
||||
selfShape.vertices[selfA]:add(self.currentOffset, _cachedSegmentA.a)
|
||||
selfShape.vertices[selfB]:add(self.currentOffset, _cachedSegmentA.b)
|
||||
|
||||
_cachedSegmentA.a:direction(_cachedSegmentA.b, self.currentNormal)
|
||||
self.currentNormal:normalize(self.currentNormal)
|
||||
self.currentNormal:left(self.currentNormal)
|
||||
|
||||
local otherA = otherInterval.indices[otherInterval.maxIndex].index
|
||||
local otherB = otherInterval.indices[otherInterval.maxIndex - 1].index
|
||||
if ((otherA == 1 or otherB == 1) and (otherA == otherShape.vertexCount or otherB == otherShape.vertexCount)) then
|
||||
otherA, otherB = math.max(otherA, otherB), math.min(otherA, otherB)
|
||||
else
|
||||
otherA, otherB = math.min(otherA, otherB), math.max(otherA, otherB)
|
||||
end
|
||||
|
||||
otherShape.vertices[otherA]:add(self.otherOffset, _cachedSegmentB.a)
|
||||
otherShape.vertices[otherB]:add(self.otherOffset, _cachedSegmentB.b)
|
||||
|
||||
_cachedSegmentB.a:direction(_cachedSegmentB.b, self.otherNormal)
|
||||
self.otherNormal:normalize(self.otherNormal)
|
||||
self.otherNormal:left(self.otherNormal)
|
||||
elseif side == SIDE_RIGHT then
|
||||
local selfA = currentInterval.indices[currentInterval.maxIndex].index
|
||||
local selfB = currentInterval.indices[currentInterval.maxIndex - 1].index
|
||||
if ((selfA == 1 or selfB == 1) and (selfA == selfShape.vertexCount or selfB == selfShape.vertexCount)) then
|
||||
selfA, selfB = math.max(selfA, selfB), math.min(selfA, selfB)
|
||||
else
|
||||
selfA, selfB = math.min(selfA, selfB), math.max(selfA, selfB)
|
||||
end
|
||||
|
||||
selfShape.vertices[selfA]:add(self.currentOffset, _cachedSegmentA.a)
|
||||
selfShape.vertices[selfB]:add(self.currentOffset, _cachedSegmentA.b)
|
||||
|
||||
_cachedSegmentA.a:direction(_cachedSegmentA.b, self.currentNormal)
|
||||
self.currentNormal:normalize(self.currentNormal)
|
||||
self.currentNormal:left(self.currentNormal)
|
||||
|
||||
local otherA = otherInterval.indices[otherInterval.minIndex].index
|
||||
local otherB = otherInterval.indices[otherInterval.minIndex + 1].index
|
||||
if ((otherA == 1 or otherB == 1) and (otherA == otherShape.vertexCount or otherB == otherShape.vertexCount)) then
|
||||
otherA, otherB = math.max(otherA, otherB), math.min(otherA, otherB)
|
||||
else
|
||||
otherA, otherB = math.min(otherA, otherB), math.max(otherA, otherB)
|
||||
end
|
||||
|
||||
otherShape.vertices[otherA]:add(self.otherOffset, _cachedSegmentB.a)
|
||||
otherShape.vertices[otherB]:add(self.otherOffset, _cachedSegmentB.b)
|
||||
|
||||
_cachedSegmentB.a:direction(_cachedSegmentB.b, self.otherNormal)
|
||||
self.otherNormal:normalize(self.otherNormal)
|
||||
self.otherNormal:left(self.otherNormal)
|
||||
end
|
||||
|
||||
self.normal:init(self.otherNormal.x, self.otherNormal.y)
|
||||
|
||||
local intersection, x, y
|
||||
if _cachedSegmentA:overlap(_cachedSegmentB) then
|
||||
intersection, x, y = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon)
|
||||
|
||||
if intersection and not (x and y) then
|
||||
intersection = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.a, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon)
|
||||
if intersection then
|
||||
self:_addContactPoint(_cachedSegmentA.a.x, _cachedSegmentB.a.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(_cachedSegmentA.b, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon)
|
||||
if intersection then
|
||||
self:_addContactPoint(_cachedSegmentA.b.x, _cachedSegmentB.b.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(_cachedSegmentB.a, _cachedSegmentB.a, _cachedSegmentA.a, _cachedSegmentA.b, self.epsilon)
|
||||
if intersection then
|
||||
self:_addContactPoint(_cachedSegmentB.a.x, _cachedSegmentB.a.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(_cachedSegmentB.b, _cachedSegmentB.b, _cachedSegmentA.a, _cachedSegmentA.b, self.epsilon)
|
||||
if intersection then
|
||||
self:_addContactPoint(_cachedSegmentB.b.x, _cachedSegmentB.b.y)
|
||||
end
|
||||
elseif intersection and x and y then
|
||||
self:_addContactPoint(x, y)
|
||||
end
|
||||
end
|
||||
elseif side == SIDE_NONE then
|
||||
for j = 1, selfShape.vertexCount do
|
||||
_cachedSegmentA:init(selfShape.vertices[j], selfShape.vertices[j % selfShape.vertexCount + 1])
|
||||
|
||||
if self.time > 0 then
|
||||
_cachedSegmentA.a:add(self.currentOffset, _cachedSegmentA.a)
|
||||
_cachedSegmentA.b:add(self.currentOffset, _cachedSegmentA.b)
|
||||
end
|
||||
|
||||
for k = 1, otherShape.vertexCount do
|
||||
_cachedSegmentB:init(otherShape.vertices[k], otherShape.vertices[k % otherShape.vertexCount + 1])
|
||||
|
||||
if self.time > 0 then
|
||||
_cachedSegmentB.a:add(self.otherOffset, _cachedSegmentB.a)
|
||||
_cachedSegmentB.b:add(self.otherOffset, _cachedSegmentB.b)
|
||||
end
|
||||
|
||||
if _cachedSegmentA:overlap(_cachedSegmentB) then
|
||||
local intersection, x, y = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon)
|
||||
if intersection and x and y then
|
||||
self:_addContactPoint(x, y)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.time = math.max(self.firstTime, 0)
|
||||
self.collision = true
|
||||
|
||||
if self.depth == math.huge then
|
||||
self.depth = 0
|
||||
end
|
||||
|
||||
if self.currentDepth == math.huge then
|
||||
self.currentDepth = 0
|
||||
end
|
||||
|
||||
if self.currentShape.shape.vertexCount == 2 then
|
||||
self:_correctLineSegmentNormals(self.currentShape, self.otherShape, self.otherOffset, otherOffset)
|
||||
end
|
||||
|
||||
if self.otherShape.shape.vertexCount == 2 then
|
||||
self:_correctLineSegmentNormals(self.otherShape, self.currentShape, self.currentOffset, selfOffset)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param index number
|
||||
--- @return slick.collision.shapeCollisionResolutionQueryAxis
|
||||
function shapeCollisionResolutionQuery:_getAxis(index)
|
||||
local axis
|
||||
if index <= self.currentShape.axesCount then
|
||||
axis = self.currentShape.axes[index]
|
||||
else
|
||||
axis = self.otherShape.axes[index - self.currentShape.axesCount]
|
||||
end
|
||||
|
||||
return axis
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param depth number
|
||||
--- @param shape slick.collision.shapeCollisionResolutionQueryShape
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function shapeCollisionResolutionQuery:_addNormal(depth, shape, x, y)
|
||||
local nextCount = self.allNormalsCount + 1
|
||||
local normal = self.allNormals[nextCount]
|
||||
if not normal then
|
||||
normal = point.new()
|
||||
self.allNormals[nextCount] = normal
|
||||
end
|
||||
|
||||
normal:init(x, y)
|
||||
normal:round(normal, self.epsilon)
|
||||
normal:normalize(normal)
|
||||
|
||||
self.depths[nextCount] = depth
|
||||
self.normalsShape[nextCount] = shape
|
||||
self.allNormalsCount = nextCount
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function shapeCollisionResolutionQuery:_addContactPoint(x, y)
|
||||
local nextCount = self.contactPointsCount + 1
|
||||
local contactPoint = self.contactPoints[nextCount]
|
||||
if not contactPoint then
|
||||
contactPoint = point.new()
|
||||
self.contactPoints[nextCount] = contactPoint
|
||||
end
|
||||
|
||||
contactPoint:init(x, y)
|
||||
|
||||
for i = 1, self.contactPointsCount do
|
||||
if contactPoint:distanceSquared(self.contactPoints[i]) < self.epsilon ^ 2 then
|
||||
return
|
||||
end
|
||||
end
|
||||
self.contactPointsCount = nextCount
|
||||
end
|
||||
|
||||
local _intervalAxisNormal = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param axis slick.collision.shapeCollisionResolutionQueryAxis
|
||||
--- @return boolean
|
||||
function shapeCollisionResolutionQuery:_compareIntervals(axis)
|
||||
local currentInterval = self.currentShape.currentInterval
|
||||
local otherInterval = self.otherShape.currentInterval
|
||||
|
||||
if not currentInterval:overlaps(otherInterval) then
|
||||
return false
|
||||
end
|
||||
|
||||
local depth = currentInterval:distance(otherInterval)
|
||||
local negate = false
|
||||
if currentInterval:contains(otherInterval) or otherInterval:contains(currentInterval) then
|
||||
local max = math.abs(currentInterval.max - otherInterval.max)
|
||||
local min = math.abs(currentInterval.min - otherInterval.min)
|
||||
|
||||
if max > min then
|
||||
negate = true
|
||||
depth = depth + min
|
||||
else
|
||||
depth = depth + max
|
||||
end
|
||||
end
|
||||
|
||||
_intervalAxisNormal:init(axis.normal.x, axis.normal.y)
|
||||
if negate then
|
||||
_intervalAxisNormal:negate(_intervalAxisNormal)
|
||||
end
|
||||
|
||||
if axis.parent == self.otherShape and slickmath.less(depth, self.otherDepth, self.epsilon) then
|
||||
if depth < self.otherDepth then
|
||||
self.otherDepth = depth
|
||||
self.otherNormal:init(_intervalAxisNormal.x, _intervalAxisNormal.y)
|
||||
self.otherAxis = axis
|
||||
end
|
||||
|
||||
self:_addNormal(depth, self.otherShape, _intervalAxisNormal.x, _intervalAxisNormal.y)
|
||||
end
|
||||
|
||||
if axis.parent == self.currentShape and slickmath.less(depth, self.currentDepth, self.epsilon) then
|
||||
if depth < self.currentDepth then
|
||||
self.currentDepth = depth
|
||||
self.currentNormal:init(_intervalAxisNormal.x, _intervalAxisNormal.y)
|
||||
self.currentAxis = axis
|
||||
end
|
||||
|
||||
self:_addNormal(depth, self.currentShape, _intervalAxisNormal.x, _intervalAxisNormal.y)
|
||||
end
|
||||
|
||||
if depth < self.depth then
|
||||
self.depth = depth
|
||||
self.normal:init(_intervalAxisNormal.x, _intervalAxisNormal.y)
|
||||
self.axis = axis
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- @param selfShape slick.collision.shapeInterface
|
||||
--- @param otherShape slick.collision.shapeInterface
|
||||
--- @param selfOffset slick.geometry.point
|
||||
--- @param otherOffset slick.geometry.point
|
||||
--- @param selfVelocity slick.geometry.point
|
||||
--- @param otherVelocity slick.geometry.point
|
||||
function shapeCollisionResolutionQuery:performProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity)
|
||||
self:_beginQuery()
|
||||
self:_performPolygonPolygonProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity)
|
||||
|
||||
if self.collision then
|
||||
self.normal:round(self.normal, self.epsilon)
|
||||
self.normal:normalize(self.normal)
|
||||
self.currentNormal:round(self.currentNormal, self.epsilon)
|
||||
self.currentNormal:normalize(self.currentNormal)
|
||||
|
||||
slicktable.clear(self.normals)
|
||||
slicktable.clear(self.alternateNormals)
|
||||
|
||||
table.insert(self.normals, self.normal)
|
||||
table.insert(self.alternateNormals, self.currentNormal)
|
||||
|
||||
for i = 1, self.allNormalsCount do
|
||||
--- @type slick.geometry.point[]?
|
||||
local normals, depth
|
||||
if self.normalsShape[i] == self.otherShape then
|
||||
normals = self.normals
|
||||
depth = self.otherDepth
|
||||
elseif self.normalsShape[i] == self.currentShape then
|
||||
normals = self.alternateNormals
|
||||
depth = self.currentDepth
|
||||
end
|
||||
|
||||
if normals and slickmath.equal(depth, self.depths[i], self.epsilon) then
|
||||
local normal = self.allNormals[i]
|
||||
|
||||
local hasNormal = false
|
||||
for _, otherNormal in ipairs(normals) do
|
||||
if otherNormal.x == normal.x and otherNormal.y == normal.y then
|
||||
hasNormal = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hasNormal then
|
||||
table.insert(normals, normal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self.collision
|
||||
end
|
||||
|
||||
--- @private
|
||||
function shapeCollisionResolutionQuery:_clear()
|
||||
self.depth = 0
|
||||
self.time = 0
|
||||
self.normal:init(0, 0)
|
||||
self.contactPointsCount = 0
|
||||
end
|
||||
|
||||
function shapeCollisionResolutionQuery:_handleAxis(axis)
|
||||
self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset)
|
||||
self:_swapShapes()
|
||||
self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset)
|
||||
self:_swapShapes()
|
||||
end
|
||||
|
||||
--- @param axis slick.collision.shapeCollisionResolutionQueryAxis
|
||||
--- @param velocity slick.geometry.point
|
||||
--- @return boolean, -1 | 0 | 1 | nil
|
||||
function shapeCollisionResolutionQuery:_handleTunnelAxis(axis, velocity)
|
||||
local speed = velocity:dot(axis.normal)
|
||||
|
||||
self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset)
|
||||
self:_swapShapes()
|
||||
self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset)
|
||||
self:_swapShapes()
|
||||
|
||||
local selfInterval = self.currentShape.currentInterval
|
||||
local otherInterval = self.otherShape.currentInterval
|
||||
|
||||
local side
|
||||
if otherInterval.max < selfInterval.min then
|
||||
if speed <= 0 then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local u = (selfInterval.min - otherInterval.max) / speed
|
||||
if u > self.firstTime then
|
||||
side = SIDE_LEFT
|
||||
self.firstTime = u
|
||||
end
|
||||
|
||||
local v = (selfInterval.max - otherInterval.min) / speed
|
||||
self.lastTime = math.min(self.lastTime, v)
|
||||
|
||||
if self.firstTime > self.lastTime then
|
||||
return false, nil
|
||||
end
|
||||
elseif selfInterval.max < otherInterval.min then
|
||||
if speed >= 0 then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local u = (selfInterval.max - otherInterval.min) / speed
|
||||
if u > self.firstTime then
|
||||
side = SIDE_RIGHT
|
||||
self.firstTime = u
|
||||
end
|
||||
|
||||
local v = (selfInterval.min - otherInterval.max) / speed
|
||||
self.lastTime = math.min(self.lastTime, v)
|
||||
else
|
||||
if speed > 0 then
|
||||
local t = (selfInterval.max - otherInterval.min) / speed
|
||||
self.lastTime = math.min(self.lastTime, t)
|
||||
|
||||
if self.firstTime > self.lastTime then
|
||||
return false, nil
|
||||
end
|
||||
elseif speed < 0 then
|
||||
local t = (selfInterval.min - otherInterval.max) / speed
|
||||
self.lastTime = math.min(self.lastTime, t)
|
||||
|
||||
if self.firstTime > self.lastTime then
|
||||
return false, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.firstTime > self.lastTime then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
return true, side
|
||||
end
|
||||
|
||||
--- @param shape slick.collision.shapeInterface
|
||||
--- @param point slick.geometry.point
|
||||
--- @return slick.geometry.point?
|
||||
function shapeCollisionResolutionQuery:getClosestVertex(shape, point)
|
||||
local minDistance
|
||||
local result
|
||||
|
||||
for i = 1, shape.vertexCount do
|
||||
local vertex = shape.vertices[i]
|
||||
local distance = vertex:distanceSquared(point)
|
||||
|
||||
if distance < (minDistance or math.huge) then
|
||||
minDistance = distance
|
||||
result = vertex
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local _cachedGetAxesCircleCenter = point.new()
|
||||
function shapeCollisionResolutionQuery:getAxes()
|
||||
--- @type slick.collision.shapeInterface
|
||||
local shape = self.currentShape.shape
|
||||
for i = 1, shape.normalCount do
|
||||
local normal = shape.normals[i]
|
||||
|
||||
local axis = self:addAxis()
|
||||
axis.normal:init(normal.x, normal.y)
|
||||
axis.segment:init(shape.vertices[(i - 1) % shape.vertexCount + 1], shape.vertices[i % shape.vertexCount + 1])
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedOffsetVertex = point.new()
|
||||
|
||||
--- @param axis slick.geometry.point
|
||||
--- @param interval slick.collision.interval
|
||||
--- @param offset slick.geometry.point?
|
||||
function shapeCollisionResolutionQuery:project(axis, interval, offset)
|
||||
for i = 1, self.currentShape.shape.vertexCount do
|
||||
local vertex = self.currentShape.shape.vertices[i]
|
||||
_cachedOffsetVertex:init(vertex.x, vertex.y)
|
||||
if offset then
|
||||
_cachedOffsetVertex:add(offset, _cachedOffsetVertex)
|
||||
end
|
||||
|
||||
interval:update(_cachedOffsetVertex:dot(axis), i)
|
||||
end
|
||||
end
|
||||
|
||||
return shapeCollisionResolutionQuery
|
||||
110
game/love_src/lib/slick/collision/shapeGroup.lua
Normal file
110
game/love_src/lib/slick/collision/shapeGroup.lua
Normal file
@ -0,0 +1,110 @@
|
||||
local cache = require("slick.cache")
|
||||
local polygonMesh = require("slick.collision.polygonMesh")
|
||||
local enum = require("slick.enum")
|
||||
local tag = require("slick.tag")
|
||||
local util = require("slick.util")
|
||||
|
||||
--- @class slick.collision.shapeGroup
|
||||
--- @field tag any
|
||||
--- @field entity slick.entity | slick.cache
|
||||
--- @field shapes slick.collision.shape[]
|
||||
local shapeGroup = {}
|
||||
local metatable = { __index = shapeGroup }
|
||||
|
||||
--- @param entity slick.entity | slick.cache
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @param ... slick.collision.shapeDefinition
|
||||
--- @return slick.collision.shapeGroup
|
||||
function shapeGroup.new(entity, tag, ...)
|
||||
local result = setmetatable({
|
||||
entity = entity,
|
||||
shapes = {}
|
||||
}, metatable)
|
||||
|
||||
result:_addShapeDefinitions(tag, ...)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param tagInstance slick.tag | slick.enum | nil
|
||||
--- @param shapeDefinition slick.collision.shapeDefinition?
|
||||
--- @param ... slick.collision.shapeDefinition
|
||||
function shapeGroup:_addShapeDefinitions(tagInstance, shapeDefinition, ...)
|
||||
if not shapeDefinition then
|
||||
return
|
||||
end
|
||||
|
||||
local shape
|
||||
if shapeDefinition.type == shapeGroup then
|
||||
shape = shapeDefinition.type.new(self.entity, shapeDefinition.tag, unpack(shapeDefinition.arguments, 1, shapeDefinition.n))
|
||||
else
|
||||
shape = shapeDefinition.type.new(self.entity, unpack(shapeDefinition.arguments, 1, shapeDefinition.n))
|
||||
end
|
||||
|
||||
local shapeTag = shapeDefinition.tag or tagInstance
|
||||
local tagValue = nil
|
||||
if util.is(shapeTag, tag) then
|
||||
tagValue = shapeTag and shapeTag.value
|
||||
elseif util.is(shapeTag, enum) then
|
||||
tagValue = shapeTag
|
||||
elseif type(shapeTag) ~= "nil" then
|
||||
error("expected tag to be an instance of slick.enum or slick.tag")
|
||||
end
|
||||
|
||||
shape.tag = tagValue
|
||||
|
||||
self:_addShapes(shape)
|
||||
return self:_addShapeDefinitions(tagInstance, ...)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param shape slick.collision.shapelike
|
||||
---@param ... slick.collision.shapelike
|
||||
function shapeGroup:_addShapes(shape, ...)
|
||||
if not shape then
|
||||
return
|
||||
end
|
||||
|
||||
if util.is(shape, shapeGroup) then
|
||||
--- @cast shape slick.collision.shapeGroup
|
||||
return self:_addShapes(unpack(shape.shapes))
|
||||
else
|
||||
table.insert(self.shapes, shape)
|
||||
return self:_addShapes(...)
|
||||
end
|
||||
end
|
||||
|
||||
function shapeGroup:attach()
|
||||
local shapes = self.shapes
|
||||
|
||||
local index = 1
|
||||
while index <= #shapes do
|
||||
local shape = shapes[index]
|
||||
if util.is(shape, polygonMesh) then
|
||||
--- @type slick.cache
|
||||
local c
|
||||
|
||||
if util.is(self.entity, cache) then
|
||||
--- @diagnostic disable-next-line: cast-local-type
|
||||
c = self.entity
|
||||
else
|
||||
c = self.entity.world.cache
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line: cast-type-mismatch
|
||||
--- @cast shape slick.collision.polygonMesh
|
||||
shape:build(c.triangulator)
|
||||
|
||||
table.remove(shapes, index)
|
||||
for i = #shape.polygons, 1, -1 do
|
||||
local polygon = shape.polygons[i]
|
||||
table.insert(shapes, index, polygon)
|
||||
end
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return shapeGroup
|
||||
178
game/love_src/lib/slick/draw.lua
Normal file
178
game/love_src/lib/slick/draw.lua
Normal file
@ -0,0 +1,178 @@
|
||||
local lineSegment = require "slick.collision.lineSegment"
|
||||
local point = require "slick.geometry.point"
|
||||
local ray = require "slick.geometry.ray"
|
||||
local rectangle = require "slick.geometry.rectangle"
|
||||
local segment = require "slick.geometry.segment"
|
||||
local util = require "slick.util"
|
||||
local worldQuery = require "slick.worldQuery"
|
||||
|
||||
--- @param node slick.collision.quadTreeNode
|
||||
local function _drawQuadTreeNode(node)
|
||||
love.graphics.rectangle("line", node.bounds:left(), node.bounds:top(), node.bounds:width(), node.bounds:height())
|
||||
|
||||
love.graphics.print(node.level, node.bounds:right() - 16, node.bounds:bottom() - 16)
|
||||
end
|
||||
|
||||
local function _defaultFilter()
|
||||
return true
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
local function _drawShapes(world)
|
||||
local items = world:getItems()
|
||||
for _, item in ipairs(items) do
|
||||
local entity = world:get(item)
|
||||
for _, shape in ipairs(entity.shapes.shapes) do
|
||||
if util.is(shape, lineSegment) then
|
||||
--- @cast shape slick.collision.lineSegment
|
||||
love.graphics.line(shape.segment.a.x, shape.segment.a.y, shape.segment.b.x, shape.segment.b.y)
|
||||
elseif shape.vertexCount == 4 then
|
||||
love.graphics.polygon("line", shape.vertices[1].x, shape.vertices[1].y, shape.vertices[2].x,
|
||||
shape.vertices[2].y, shape.vertices[3].x, shape.vertices[3].y, shape.vertices[4].x,
|
||||
shape.vertices[4].y)
|
||||
else
|
||||
for i = 1, shape.vertexCount do
|
||||
local j = i % shape.vertexCount + 1
|
||||
|
||||
local a = shape.vertices[i]
|
||||
local b = shape.vertices[j]
|
||||
|
||||
love.graphics.line(a.x, a.y, b.x, b.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
local function _drawText(world)
|
||||
local items = world:getItems()
|
||||
for _, item in ipairs(items) do
|
||||
local entity = world:get(item)
|
||||
for _, shape in ipairs(entity.shapes.shapes) do
|
||||
love.graphics.print(string.format("%.2f, %.2f", shape.bounds.topLeft.x, shape.bounds.topLeft.y), shape.vertices[1].x, shape.vertices[1].y)
|
||||
love.graphics.print(string.format("%.2f x %.2f", shape.bounds:width(), shape.bounds:height()), shape.vertices[1].x, shape.vertices[1].y + 8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
local function _drawNormals(world)
|
||||
local items = world:getItems()
|
||||
for _, item in ipairs(items) do
|
||||
local entity = world:get(item)
|
||||
for _, shape in ipairs(entity.shapes.shapes) do
|
||||
local localSize = math.max(shape.bounds:width(), shape.bounds:height()) / 8
|
||||
|
||||
for i = 1, shape.vertexCount do
|
||||
local j = i % shape.vertexCount + 1
|
||||
|
||||
local a = shape.vertices[i]
|
||||
local b = shape.vertices[j]
|
||||
|
||||
if i <= shape.normalCount then
|
||||
local n = shape.normals[i]
|
||||
love.graphics.line((a.x + b.x) / 2, (a.y + b.y) / 2, (a.x + b.x) / 2 + n.x * localSize, (a.y + b.y) / 2 + n.y * localSize)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @class slick.draw.options
|
||||
--- @field text boolean?
|
||||
--- @field quadTree boolean?
|
||||
--- @field normals boolean?
|
||||
local defaultOptions = {
|
||||
text = true,
|
||||
quadTree = true,
|
||||
normals = true
|
||||
}
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param queries { filter: slick.worldShapeFilterQueryFunc, shape: slick.geometry.shape }[]?
|
||||
--- @param options slick.draw.options?
|
||||
local function draw(world, queries, options)
|
||||
options = options or defaultOptions
|
||||
local drawText = options.text == nil and defaultOptions.text or options.text
|
||||
local drawQuadTree = options.quadTree == nil and defaultOptions.quadTree or options.quadTree
|
||||
local drawNormals = options.normals == nil and defaultOptions.normals or options.normals
|
||||
|
||||
local bounds = rectangle.new(world.quadTree:computeExactBounds())
|
||||
local size = math.min(bounds:width(), bounds:height()) / 16
|
||||
|
||||
love.graphics.push("all")
|
||||
|
||||
local cr, cg, cb, ca = love.graphics.getColor()
|
||||
|
||||
_drawShapes(world)
|
||||
|
||||
if drawNormals then
|
||||
love.graphics.setColor(0, 1, 0, ca)
|
||||
_drawNormals(world)
|
||||
love.graphics.setColor(cr, cg, cb, ca)
|
||||
end
|
||||
|
||||
if drawText then
|
||||
_drawText(world)
|
||||
end
|
||||
|
||||
if queries then
|
||||
local query = worldQuery.new(world)
|
||||
for _, q in ipairs(queries) do
|
||||
local shape = q.shape
|
||||
local filter = q.filter
|
||||
|
||||
love.graphics.setColor(0, 0.5, 1, 0.5)
|
||||
if util.is(shape, point) then
|
||||
--- @cast shape slick.geometry.point
|
||||
love.graphics.circle("fill", shape.x, shape.y, 4)
|
||||
elseif util.is(shape, ray) then
|
||||
--- @cast shape slick.geometry.ray
|
||||
love.graphics.line(shape.origin.x, shape.origin.y, shape.origin.x + shape.direction.x * size, shape.origin.y + shape.direction.y * size)
|
||||
|
||||
local left = point.new()
|
||||
shape.direction:left(left)
|
||||
|
||||
local right = point.new()
|
||||
shape.direction:right(right)
|
||||
|
||||
love.graphics.line(
|
||||
shape.origin.x + shape.direction.x * (size / 2) - left.x * (size / 2),
|
||||
shape.origin.y + shape.direction.y * (size / 2) - left.y * (size / 2),
|
||||
shape.origin.x + shape.direction.x * size,
|
||||
shape.origin.y + shape.direction.y * size)
|
||||
love.graphics.line(
|
||||
shape.origin.x + shape.direction.x * (size / 2) - right.x * (size / 2),
|
||||
shape.origin.y + shape.direction.y * (size / 2) - right.y * (size / 2),
|
||||
shape.origin.x + shape.direction.x * size,
|
||||
shape.origin.y + shape.direction.y * size)
|
||||
elseif util.is(shape, rectangle) then
|
||||
--- @cast shape slick.geometry.rectangle
|
||||
love.graphics.rectangle("line", shape:left(), shape:top(), shape:width(), shape:height())
|
||||
elseif util.is(shape, segment) then
|
||||
--- @cast shape slick.geometry.segment
|
||||
love.graphics.line(shape.a.x, shape.a.y, shape.b.x, shape.b.y)
|
||||
end
|
||||
|
||||
query:performPrimitive(shape, filter or _defaultFilter)
|
||||
|
||||
love.graphics.setColor(1, 0, 0, 1)
|
||||
for _, result in ipairs(query.results) do
|
||||
love.graphics.rectangle("fill", result.contactPoint.x - 2, result.contactPoint.y - 2, 4, 4)
|
||||
for _, contact in ipairs(result.contactPoints) do
|
||||
love.graphics.rectangle("fill", contact.x - 2, contact.y - 2, 4, 4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
love.graphics.setColor(0, 1, 1, 0.5)
|
||||
if drawQuadTree then
|
||||
world.quadTree.root:visit(_drawQuadTreeNode)
|
||||
end
|
||||
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
return draw
|
||||
105
game/love_src/lib/slick/entity.lua
Normal file
105
game/love_src/lib/slick/entity.lua
Normal file
@ -0,0 +1,105 @@
|
||||
local shapeGroup = require("slick.collision.shapeGroup")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local transform = require("slick.geometry.transform")
|
||||
|
||||
--- @class slick.entity
|
||||
--- @field item any?
|
||||
--- @field world slick.world?
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field shapes slick.collision.shapeGroup
|
||||
--- @field transform slick.geometry.transform
|
||||
local entity = {}
|
||||
local metatable = { __index = entity }
|
||||
|
||||
--- @return slick.entity
|
||||
function entity.new()
|
||||
local result = setmetatable({ transform = transform.new(), bounds = rectangle.new() }, metatable)
|
||||
result.shapes = shapeGroup.new(result)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function entity:init(item)
|
||||
self.item = item
|
||||
self.shapes = shapeGroup.new(self)
|
||||
self.bounds:init(0, 0, 0, 0)
|
||||
|
||||
transform.IDENTITY:copy(self.transform)
|
||||
end
|
||||
|
||||
function entity:_updateBounds()
|
||||
local shapes = self.shapes.shapes
|
||||
if #shapes == 0 then
|
||||
self.bounds:init(0, 0, 0, 0)
|
||||
return
|
||||
end
|
||||
|
||||
self.bounds:init(shapes[1].bounds:left(), shapes[1].bounds:top(), shapes[1].bounds:right(), shapes[1].bounds:bottom())
|
||||
for i = 2, #self.shapes.shapes do
|
||||
self.bounds:expand(shapes[i].bounds:left(), shapes[i].bounds:top())
|
||||
self.bounds:expand(shapes[i].bounds:right(), shapes[i].bounds:bottom())
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
function entity:_updateQuadTree()
|
||||
if not self.world then
|
||||
return
|
||||
end
|
||||
|
||||
local shapes = self.shapes.shapes
|
||||
for _, shape in ipairs(shapes) do
|
||||
shape:transform(self.transform)
|
||||
end
|
||||
self:_updateBounds()
|
||||
|
||||
for _, shape in ipairs(self.shapes.shapes) do
|
||||
--- @cast shape slick.collision.shape
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self.world:_addShape(shape)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param ... slick.collision.shapeDefinition
|
||||
function entity:setShapes(...)
|
||||
if self.world then
|
||||
for _, shape in ipairs(self.shapes.shapes) do
|
||||
--- @cast shape slick.collision.shape
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self.world:_removeShape(shape)
|
||||
end
|
||||
end
|
||||
|
||||
self.shapes = shapeGroup.new(self, nil, ...)
|
||||
if self.world then
|
||||
self.shapes:attach()
|
||||
self:_updateQuadTree()
|
||||
end
|
||||
end
|
||||
|
||||
--- @param transform slick.geometry.transform
|
||||
function entity:setTransform(transform)
|
||||
transform:copy(self.transform)
|
||||
self:_updateQuadTree()
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
function entity:add(world)
|
||||
self.world = world
|
||||
self.shapes:attach()
|
||||
self:_updateQuadTree()
|
||||
end
|
||||
|
||||
function entity:detach()
|
||||
if self.world then
|
||||
for _, shape in ipairs(self.shapes.shapes) do
|
||||
--- @cast shape slick.collision.shape
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self.world:_removeShape(shape)
|
||||
end
|
||||
end
|
||||
|
||||
self.item = nil
|
||||
end
|
||||
|
||||
return entity
|
||||
12
game/love_src/lib/slick/enum.lua
Normal file
12
game/love_src/lib/slick/enum.lua
Normal file
@ -0,0 +1,12 @@
|
||||
--- @class slick.enum
|
||||
--- @field value any
|
||||
local enum = {}
|
||||
local metatable = { __index = enum }
|
||||
|
||||
function enum.new(value)
|
||||
return setmetatable({
|
||||
value = value
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
return enum
|
||||
909
game/love_src/lib/slick/geometry/clipper.lua
Normal file
909
game/love_src/lib/slick/geometry/clipper.lua
Normal file
@ -0,0 +1,909 @@
|
||||
local quadTree = require "slick.collision.quadTree"
|
||||
local quadTreeQuery = require "slick.collision.quadTreeQuery"
|
||||
local merge = require "slick.geometry.merge"
|
||||
local point = require "slick.geometry.point"
|
||||
local rectangle = require "slick.geometry.rectangle"
|
||||
local segment = require "slick.geometry.segment"
|
||||
local delaunay = require "slick.geometry.triangulation.delaunay"
|
||||
local edge = require "slick.geometry.triangulation.edge"
|
||||
local slicktable = require "slick.util.slicktable"
|
||||
local pool = require "slick.util.pool"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
local search = require "slick.util.search"
|
||||
|
||||
local function _compareNumber(a, b)
|
||||
return a - b
|
||||
end
|
||||
|
||||
--- @alias slick.geometry.clipper.clipOperation fun(self: slick.geometry.clipper, a: number, b: number)
|
||||
|
||||
--- @alias slick.geometry.clipper.polygonUserdata {
|
||||
--- userdata: any,
|
||||
--- polygons: table<slick.geometry.clipper.polygon, number[]>,
|
||||
--- parent: slick.geometry.clipper.polygon,
|
||||
--- hasEdge: boolean,
|
||||
--- isExteriorEdge: boolean,
|
||||
--- isInteriorEdge: boolean,
|
||||
--- }
|
||||
|
||||
--- @alias slick.geometry.clipper.polygon {
|
||||
--- points: number[],
|
||||
--- edges: number[],
|
||||
--- combinedEdges: number[],
|
||||
--- interior: number[],
|
||||
--- exterior: number[],
|
||||
--- userdata: any[],
|
||||
--- triangles: number[][],
|
||||
--- triangleCount: number,
|
||||
--- polygons: number[][],
|
||||
--- polygonCount: number,
|
||||
--- pointToCombinedPointIndex: table<number, number>,
|
||||
--- combinedPointToPointIndex: table<number, number>,
|
||||
--- quadTreeOptions: slick.collision.quadTreeOptions,
|
||||
--- quadTree: slick.collision.quadTree,
|
||||
--- quadTreeQuery: slick.collision.quadTreeQuery,
|
||||
--- bounds: slick.geometry.rectangle,
|
||||
--- }
|
||||
|
||||
--- @param quadTreeOptions slick.collision.quadTreeOptions?
|
||||
--- @return slick.geometry.clipper.polygon
|
||||
local function _newPolygon(quadTreeOptions)
|
||||
local quadTree = quadTree.new(quadTreeOptions)
|
||||
local quadTreeQuery = quadTreeQuery.new(quadTree)
|
||||
|
||||
return {
|
||||
points = {},
|
||||
edges = {},
|
||||
combinedEdges = {},
|
||||
exterior = {},
|
||||
interior = {},
|
||||
userdata = {},
|
||||
triangles = {},
|
||||
triangleCount = 0,
|
||||
polygons = {},
|
||||
polygonCount = 0,
|
||||
pointToCombinedPointIndex = {},
|
||||
combinedPointToPointIndex = {},
|
||||
quadTreeOptions = {
|
||||
maxLevels = quadTreeOptions and quadTreeOptions.maxLevels,
|
||||
maxData = quadTreeOptions and quadTreeOptions.maxData,
|
||||
expand = false
|
||||
},
|
||||
quadTree = quadTree,
|
||||
quadTreeQuery = quadTreeQuery,
|
||||
bounds = rectangle.new(),
|
||||
prepareCleanupOptions = {}
|
||||
}
|
||||
end
|
||||
|
||||
--- @class slick.geometry.clipper
|
||||
--- @field private innerPolygonsPool slick.util.pool
|
||||
--- @field private combinedPoints number[]
|
||||
--- @field private combinedEdges number[]
|
||||
--- @field private combinedUserdata slick.geometry.clipper.polygonUserdata[]
|
||||
--- @field private merge slick.geometry.clipper.merge
|
||||
--- @field private triangulator slick.geometry.triangulation.delaunay
|
||||
--- @field private pendingPolygonEdges number[]
|
||||
--- @field private cachedEdge slick.geometry.triangulation.edge
|
||||
--- @field private edges slick.geometry.triangulation.edge[]
|
||||
--- @field private edgesPool slick.util.pool
|
||||
--- @field private subjectPolygon slick.geometry.clipper.polygon
|
||||
--- @field private otherPolygon slick.geometry.clipper.polygon
|
||||
--- @field private resultPolygon slick.geometry.clipper.polygon
|
||||
--- @field private cachedPoint slick.geometry.point
|
||||
--- @field private cachedSegment slick.geometry.segment
|
||||
--- @field private clipCleanupOptions slick.geometry.clipper.clipOptions
|
||||
--- @field private inputCleanupOptions slick.geometry.clipper.clipOptions?
|
||||
--- @field private indexToResultIndex table<number, number>
|
||||
--- @field private resultPoints number[]?
|
||||
--- @field private resultEdges number[]?
|
||||
--- @field private resultUserdata any[]?
|
||||
--- @field private resultExteriorEdges number[]?
|
||||
--- @field private resultInteriorEdges number[]?
|
||||
--- @field private resultIndex number
|
||||
local clipper = {}
|
||||
local metatable = { __index = clipper }
|
||||
|
||||
--- @param triangulator slick.geometry.triangulation.delaunay?
|
||||
--- @param quadTreeOptions slick.collision.quadTreeOptions?
|
||||
--- @return slick.geometry.clipper
|
||||
function clipper.new(triangulator, quadTreeOptions)
|
||||
local self = {
|
||||
triangulator = triangulator or delaunay.new(),
|
||||
|
||||
combinedPoints = {},
|
||||
combinedEdges = {},
|
||||
combinedUserdata = {},
|
||||
merge = merge.new(),
|
||||
|
||||
innerPolygonsPool = pool.new(),
|
||||
|
||||
pendingPolygonEdges = {},
|
||||
|
||||
cachedEdge = edge.new(),
|
||||
edges = {},
|
||||
edgesPool = pool.new(edge),
|
||||
|
||||
subjectPolygon = _newPolygon(quadTreeOptions),
|
||||
otherPolygon = _newPolygon(quadTreeOptions),
|
||||
resultPolygon = _newPolygon(quadTreeOptions),
|
||||
|
||||
cachedPoint = point.new(),
|
||||
cachedSegment = segment.new(),
|
||||
|
||||
clipCleanupOptions = {},
|
||||
|
||||
indexToResultIndex = {},
|
||||
resultIndex = 1
|
||||
}
|
||||
|
||||
--- @cast self slick.geometry.clipper
|
||||
--- @param intersection slick.geometry.triangulation.intersection
|
||||
function self.clipCleanupOptions.intersect(intersection)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self:_intersect(intersection)
|
||||
end
|
||||
|
||||
function self.clipCleanupOptions.dissolve(dissolve)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
self:_dissolve(dissolve)
|
||||
end
|
||||
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param t table<slick.geometry.clipper.polygon, number[]>
|
||||
--- @param other table<slick.geometry.clipper.polygon, number[]>
|
||||
--- @param ... table<slick.geometry.clipper.polygon, number[]>
|
||||
--- @return table<slick.geometry.clipper.polygon, number[]>
|
||||
function clipper:_mergePolygonSet(t, other, ...)
|
||||
if not other then
|
||||
return t
|
||||
end
|
||||
|
||||
for k, v in pairs(other) do
|
||||
if not t[k] then
|
||||
t[k] = self.innerPolygonsPool:allocate()
|
||||
slicktable.clear(t[k])
|
||||
end
|
||||
|
||||
for _, p in ipairs(v) do
|
||||
local i = search.lessThan(t[k], p, _compareNumber) + 1
|
||||
if t[k][i] ~= p then
|
||||
table.insert(t[k], i, p)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self:_mergePolygonSet(t, ...)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param intersection slick.geometry.triangulation.intersection
|
||||
function clipper:_intersect(intersection)
|
||||
local a1, b1 = intersection.a1Userdata, intersection.b1Userdata
|
||||
local a2, b2 = intersection.a2Userdata, intersection.b2Userdata
|
||||
|
||||
if self.inputCleanupOptions and self.inputCleanupOptions.intersect then
|
||||
intersection.a1Userdata = a1.userdata
|
||||
intersection.b1Userdata = b1.userdata
|
||||
|
||||
intersection.a2Userdata = a2.userdata
|
||||
intersection.b2Userdata = b2.userdata
|
||||
|
||||
self.inputCleanupOptions.intersect(intersection)
|
||||
|
||||
intersection.a1Userdata, intersection.b1Userdata = a1, b1
|
||||
intersection.a2Userdata, intersection.b2Userdata = a2, b2
|
||||
end
|
||||
|
||||
local userdata = self.combinedUserdata[intersection.resultIndex]
|
||||
if not userdata then
|
||||
userdata = { polygons = {} }
|
||||
self.combinedUserdata[intersection.resultIndex] = userdata
|
||||
else
|
||||
slicktable.clear(userdata.polygons)
|
||||
userdata.parent = nil
|
||||
end
|
||||
|
||||
userdata.userdata = intersection.resultUserdata
|
||||
userdata.isExteriorEdge = userdata.isExteriorEdge or
|
||||
intersection.a1Userdata.isExteriorEdge or
|
||||
intersection.a2Userdata.isExteriorEdge or
|
||||
intersection.b1Userdata.isExteriorEdge or
|
||||
intersection.b2Userdata.isExteriorEdge
|
||||
userdata.isInteriorEdge = userdata.isInteriorEdge or
|
||||
intersection.a1Userdata.isInteriorEdge or
|
||||
intersection.a2Userdata.isInteriorEdge or
|
||||
intersection.b1Userdata.isInteriorEdge or
|
||||
intersection.b2Userdata.isInteriorEdge
|
||||
|
||||
self:_mergePolygonSet(userdata.polygons, a1.polygons, b1.polygons, a2.polygons, b2.polygons)
|
||||
|
||||
intersection.resultUserdata = userdata
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param dissolve slick.geometry.triangulation.dissolve
|
||||
function clipper:_dissolve(dissolve)
|
||||
if self.inputCleanupOptions and self.inputCleanupOptions.dissolve then
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local u = dissolve.userdata
|
||||
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local o = dissolve.otherUserdata
|
||||
|
||||
dissolve.userdata = u.userdata
|
||||
dissolve.otherUserdata = o.userdata
|
||||
|
||||
self.inputCleanupOptions.dissolve(dissolve)
|
||||
|
||||
dissolve.userdata = u
|
||||
dissolve.otherUserdata = o
|
||||
|
||||
if dissolve.resultUserdata ~= nil then
|
||||
o.userdata = dissolve.resultUserdata
|
||||
dissolve.resultUserdata = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function clipper:reset()
|
||||
self.edgesPool:reset()
|
||||
self.innerPolygonsPool:reset()
|
||||
|
||||
slicktable.clear(self.subjectPolygon.points)
|
||||
slicktable.clear(self.subjectPolygon.edges)
|
||||
slicktable.clear(self.subjectPolygon.userdata)
|
||||
|
||||
slicktable.clear(self.otherPolygon.points)
|
||||
slicktable.clear(self.otherPolygon.edges)
|
||||
slicktable.clear(self.otherPolygon.userdata)
|
||||
|
||||
slicktable.clear(self.combinedPoints)
|
||||
slicktable.clear(self.combinedEdges)
|
||||
|
||||
slicktable.clear(self.edges)
|
||||
slicktable.clear(self.pendingPolygonEdges)
|
||||
|
||||
slicktable.clear(self.indexToResultIndex)
|
||||
self.resultIndex = 1
|
||||
|
||||
self.inputCleanupOptions = nil
|
||||
|
||||
self.resultPoints = nil
|
||||
self.resultEdges = nil
|
||||
self.resultUserdata = nil
|
||||
end
|
||||
|
||||
--- @type slick.geometry.triangulation.delaunayTriangulationOptions
|
||||
local _triangulateOptions = {
|
||||
refine = true,
|
||||
interior = true,
|
||||
exterior = false,
|
||||
polygonization = true
|
||||
}
|
||||
|
||||
local _cachedPolygonBounds = rectangle.new()
|
||||
|
||||
--- @private
|
||||
--- @param points number[]
|
||||
--- @param exterior number[]?
|
||||
--- @param interior number[]?
|
||||
--- @param userdata any[]?
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
function clipper:_addPolygon(points, exterior, interior, userdata, polygon)
|
||||
slicktable.clear(polygon.combinedEdges)
|
||||
slicktable.clear(polygon.exterior)
|
||||
slicktable.clear(polygon.interior)
|
||||
|
||||
if exterior then
|
||||
for _, e in ipairs(exterior) do
|
||||
table.insert(polygon.exterior, e)
|
||||
table.insert(polygon.combinedEdges, e)
|
||||
end
|
||||
end
|
||||
|
||||
if interior then
|
||||
for _, e in ipairs(interior) do
|
||||
table.insert(polygon.interior, e)
|
||||
table.insert(polygon.combinedEdges, e)
|
||||
end
|
||||
end
|
||||
|
||||
if userdata then
|
||||
for _, u in ipairs(userdata) do
|
||||
table.insert(polygon.userdata, u)
|
||||
end
|
||||
end
|
||||
|
||||
self.triangulator:clean(points, polygon.exterior, nil, nil, polygon.points, polygon.edges)
|
||||
local _, triangleCount, _, polygonCount = self.triangulator:triangulate(polygon.points, polygon.edges, _triangulateOptions, polygon.triangles, polygon.polygons)
|
||||
|
||||
polygon.triangleCount = triangleCount
|
||||
polygon.polygonCount = polygonCount or 0
|
||||
|
||||
if #polygon.points > 0 then
|
||||
polygon.bounds:init(polygon.points[1], polygon.points[2])
|
||||
|
||||
for i = 3, #polygon.points, 2 do
|
||||
polygon.bounds:expand(polygon.points[i], polygon.points[i + 1])
|
||||
end
|
||||
else
|
||||
polygon.bounds:init(0, 0, 0, 0)
|
||||
end
|
||||
|
||||
polygon.quadTreeOptions.x = polygon.bounds:left()
|
||||
polygon.quadTreeOptions.y = polygon.bounds:top()
|
||||
polygon.quadTreeOptions.width = math.max(polygon.bounds:width(), self.triangulator.epsilon)
|
||||
polygon.quadTreeOptions.height = math.max(polygon.bounds:height(), self.triangulator.epsilon)
|
||||
|
||||
polygon.quadTree:clear()
|
||||
polygon.quadTree:rebuild(polygon.quadTreeOptions)
|
||||
|
||||
for i = 1, polygon.polygonCount do
|
||||
local p = polygon.polygons[i]
|
||||
|
||||
_cachedPolygonBounds.topLeft:init(math.huge, math.huge)
|
||||
_cachedPolygonBounds.bottomRight:init(-math.huge, -math.huge)
|
||||
|
||||
for _, vertex in ipairs(p) do
|
||||
local xIndex = (vertex - 1) * 2 + 1
|
||||
local yIndex = xIndex + 1
|
||||
|
||||
_cachedPolygonBounds:expand(polygon.points[xIndex], polygon.points[yIndex])
|
||||
end
|
||||
|
||||
polygon.quadTree:insert(p, _cachedPolygonBounds)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
function clipper:_preparePolygon(points, polygon)
|
||||
local numPoints = #self.combinedPoints / 2
|
||||
for i = 1, #points, 2 do
|
||||
local x = points[i]
|
||||
local y = points[i + 1]
|
||||
|
||||
table.insert(self.combinedPoints, x)
|
||||
table.insert(self.combinedPoints, y)
|
||||
|
||||
local vertexIndex = (i + 1) / 2
|
||||
local combinedIndex = vertexIndex + numPoints
|
||||
local userdata = self.combinedUserdata[combinedIndex]
|
||||
if not userdata then
|
||||
userdata = { polygons = {} }
|
||||
self.combinedUserdata[combinedIndex] = userdata
|
||||
else
|
||||
slicktable.clear(userdata.polygons)
|
||||
end
|
||||
|
||||
userdata.parent = polygon
|
||||
userdata.polygons[polygon] = self.innerPolygonsPool:allocate()
|
||||
slicktable.clear(userdata.polygons[polygon])
|
||||
|
||||
userdata.userdata = polygon.userdata[vertexIndex]
|
||||
userdata.hasEdge = false
|
||||
|
||||
local index = (i - 1) / 2 + 1
|
||||
polygon.pointToCombinedPointIndex[index] = combinedIndex
|
||||
polygon.combinedPointToPointIndex[combinedIndex] = index
|
||||
end
|
||||
|
||||
for i = 1, polygon.polygonCount do
|
||||
local p = polygon.polygons[i]
|
||||
|
||||
for _, vertexIndex in ipairs(p) do
|
||||
local combinedIndex = vertexIndex + numPoints
|
||||
local userdata = self.combinedUserdata[combinedIndex]
|
||||
if userdata then
|
||||
local polygons = userdata.polygons[polygon]
|
||||
local innerPolygonIndex = search.lessThan(polygons, i, _compareNumber) + 1
|
||||
if polygons[innerPolygonIndex] ~= i then
|
||||
table.insert(polygons, innerPolygonIndex, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #polygon.exterior, 2 do
|
||||
local a = polygon.exterior[i] + numPoints
|
||||
local b = polygon.exterior[i + 1] + numPoints
|
||||
|
||||
self.combinedUserdata[a].hasEdge = true
|
||||
self.combinedUserdata[a].isExteriorEdge = true
|
||||
self.combinedUserdata[b].hasEdge = true
|
||||
self.combinedUserdata[b].isExteriorEdge = true
|
||||
|
||||
table.insert(self.combinedEdges, a)
|
||||
table.insert(self.combinedEdges, b)
|
||||
end
|
||||
|
||||
for i = 1, #polygon.interior, 2 do
|
||||
local a = polygon.interior[i] + numPoints
|
||||
local b = polygon.interior[i + 1] + numPoints
|
||||
|
||||
self.combinedUserdata[a].hasEdge = true
|
||||
self.combinedUserdata[a].isInteriorEdge = true
|
||||
self.combinedUserdata[b].hasEdge = true
|
||||
self.combinedUserdata[b].isInteriorEdge = true
|
||||
|
||||
table.insert(self.combinedEdges, a)
|
||||
table.insert(self.combinedEdges, b)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
function clipper:_finishPolygon(polygon)
|
||||
slicktable.clear(polygon.userdata)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param operation slick.geometry.clipper.clipOperation
|
||||
function clipper:_mergePoints(operation)
|
||||
for i = 1, #self.resultPolygon.points, 2 do
|
||||
local index = (i - 1) / 2 + 1
|
||||
local combinedUserdata = self.resultPolygon.userdata[index]
|
||||
|
||||
local x = self.resultPolygon.points[i]
|
||||
local y = self.resultPolygon.points[i + 1]
|
||||
|
||||
if not combinedUserdata.hasEdge then
|
||||
if operation == self.difference and not self:_pointInside(x, y, self.otherPolygon) then
|
||||
self:_addResultEdge(index)
|
||||
elseif operation == self.union then
|
||||
self:_addResultEdge(index)
|
||||
elseif operation == self.intersection and (self:_pointInside(x, y, self.subjectPolygon) and self:_pointInside(x, y, self.otherPolygon)) then
|
||||
self:_addResultEdge(index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
function clipper:_mergeUserdata()
|
||||
if not (self.inputCleanupOptions and self.inputCleanupOptions.merge) then
|
||||
return
|
||||
end
|
||||
|
||||
local n = #self.combinedPoints / 2
|
||||
for i = 1, n do
|
||||
local combinedUserdata = self.combinedUserdata[i]
|
||||
|
||||
if combinedUserdata.parent then
|
||||
if combinedUserdata.parent == self.subjectPolygon then
|
||||
self.merge:init(
|
||||
"subject",
|
||||
self.subjectPolygon.combinedPointToPointIndex[i],
|
||||
self.subjectPolygon.userdata[self.subjectPolygon.combinedPointToPointIndex[i]],
|
||||
i)
|
||||
elseif combinedUserdata.parent == self.otherPolygon then
|
||||
self.merge:init(
|
||||
"other",
|
||||
self.otherPolygon.combinedPointToPointIndex[i],
|
||||
self.otherPolygon.userdata[self.otherPolygon.combinedPointToPointIndex[i]],
|
||||
i)
|
||||
end
|
||||
|
||||
self.inputCleanupOptions.merge(self.merge)
|
||||
|
||||
if self.merge.resultUserdata ~= nil then
|
||||
self.resultUserdata[self.merge.resultIndex] = self.merge.resultUserdata
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
function clipper:_segmentInsidePolygon(s, polygon, vertices)
|
||||
local isABIntersection, isABCollinear = false, false
|
||||
for i = 1, #vertices do
|
||||
local j = slickmath.wrap(i, 1, #vertices)
|
||||
|
||||
local aIndex = (vertices[i] - 1) * 2 + 1
|
||||
local bIndex = (vertices[j] - 1) * 2 + 1
|
||||
|
||||
local ax = polygon.points[aIndex]
|
||||
local ay = polygon.points[aIndex + 1]
|
||||
local bx = polygon.points[bIndex]
|
||||
local by = polygon.points[bIndex + 1]
|
||||
|
||||
self.cachedSegment.a:init(ax, ay)
|
||||
self.cachedSegment.b:init(bx, by)
|
||||
|
||||
isABCollinear = isABCollinear or slickmath.collinear(self.cachedSegment.a, self.cachedSegment.b, s.a, s.b, self.triangulator.epsilon)
|
||||
|
||||
local intersection, _, _, u, v = slickmath.intersection(self.cachedSegment.a, self.cachedSegment.b, s.a, s.b, self.triangulator.epsilon)
|
||||
if intersection and u and v and (u > self.triangulator.epsilon and u + self.triangulator.epsilon < 1) and (v > self.triangulator.epsilon and v + self.triangulator.epsilon < 1) then
|
||||
isABIntersection = true
|
||||
end
|
||||
end
|
||||
|
||||
local isAInside, isACollinear = self:_pointInsidePolygon(s.a, polygon, vertices)
|
||||
local isBInside, isBCollinear = self:_pointInsidePolygon(s.b, polygon, vertices)
|
||||
|
||||
local isABInside = (isAInside or isACollinear) and (isBInside or isBCollinear)
|
||||
|
||||
return isABIntersection or isABInside, isABCollinear, isAInside, isBInside
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param p slick.geometry.point
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
--- @param vertices number[]
|
||||
--- @return boolean, boolean
|
||||
function clipper:_pointInsidePolygon(p, polygon, vertices)
|
||||
local isCollinear = false
|
||||
|
||||
local px = p.x
|
||||
local py = p.y
|
||||
|
||||
local minDistance = math.huge
|
||||
|
||||
local isInside = false
|
||||
for i = 1, #vertices do
|
||||
local j = slickmath.wrap(i, 1, #vertices)
|
||||
|
||||
local aIndex = (vertices[i] - 1) * 2 + 1
|
||||
local bIndex = (vertices[j] - 1) * 2 + 1
|
||||
|
||||
local ax = polygon.points[aIndex]
|
||||
local ay = polygon.points[aIndex + 1]
|
||||
local bx = polygon.points[bIndex]
|
||||
local by = polygon.points[bIndex + 1]
|
||||
|
||||
self.cachedSegment.a:init(ax, ay)
|
||||
self.cachedSegment.b:init(bx, by)
|
||||
|
||||
isCollinear = isCollinear or slickmath.collinear(self.cachedSegment.a, self.cachedSegment.b, p, p, self.triangulator.epsilon)
|
||||
minDistance = math.min(self.cachedSegment:distance(p), minDistance)
|
||||
|
||||
local z = (bx - ax) * (py - ay) / (by - ay) + ax
|
||||
if ((ay > py) ~= (by > py) and px < z) then
|
||||
isInside = not isInside
|
||||
end
|
||||
end
|
||||
|
||||
return isInside and minDistance > self.triangulator.epsilon, isCollinear or minDistance < self.triangulator.epsilon
|
||||
end
|
||||
|
||||
|
||||
local _cachedInsidePoint = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
function clipper:_pointInside(x, y, polygon)
|
||||
_cachedInsidePoint:init(x, y)
|
||||
polygon.quadTreeQuery:perform(_cachedInsidePoint, self.triangulator.epsilon)
|
||||
|
||||
local isInside, isCollinear
|
||||
for _, result in ipairs(polygon.quadTreeQuery.results) do
|
||||
--- @cast result number[]
|
||||
local i, c = self:_pointInsidePolygon(_cachedInsidePoint, polygon, result)
|
||||
|
||||
isInside = isInside or i
|
||||
isCollinear = isCollinear or c
|
||||
end
|
||||
|
||||
return isInside, isCollinear
|
||||
end
|
||||
|
||||
local _cachedInsideSegment = segment.new()
|
||||
|
||||
--- @private
|
||||
--- @param ax number
|
||||
--- @param ay number
|
||||
--- @param bx number
|
||||
--- @param by number
|
||||
--- @param polygon slick.geometry.clipper.polygon
|
||||
function clipper:_segmentInside(ax, ay, bx, by, polygon)
|
||||
_cachedInsideSegment.a:init(ax, ay)
|
||||
_cachedInsideSegment.b:init(bx, by)
|
||||
polygon.quadTreeQuery:perform(_cachedInsideSegment, self.triangulator.epsilon)
|
||||
|
||||
local intersection, collinear, aInside, bInside = false, false, false, false
|
||||
for _, result in ipairs(polygon.quadTreeQuery.results) do
|
||||
--- @cast result number[]
|
||||
local i, c, a, b = self:_segmentInsidePolygon(_cachedInsideSegment, polygon, result)
|
||||
intersection = intersection or i
|
||||
collinear = collinear or c
|
||||
aInside = aInside or a
|
||||
bInside = bInside or b
|
||||
end
|
||||
|
||||
return intersection or (aInside and bInside), collinear
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param segment slick.geometry.segment
|
||||
--- @param side -1 | 0 | 1
|
||||
--- @param parentPolygon slick.geometry.clipper.polygon
|
||||
--- @param childPolygons number[]
|
||||
--- @param ... number[]
|
||||
function clipper:_hasAnyOnSideImpl(segment, side, parentPolygon, childPolygons, ...)
|
||||
if not childPolygons and select("#", ...) == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
if childPolygons then
|
||||
for _, childPolygonIndex in ipairs(childPolygons) do
|
||||
local childPolygon = parentPolygon.polygons[childPolygonIndex]
|
||||
|
||||
for i = 1, #childPolygon do
|
||||
local xIndex = (childPolygon[i] - 1) * 2 + 1
|
||||
local yIndex = xIndex + 1
|
||||
|
||||
local x, y = parentPolygon.points[xIndex], parentPolygon.points[yIndex]
|
||||
self.cachedPoint:init(x, y)
|
||||
local otherSide = slickmath.direction(segment.a, segment.b, self.cachedPoint, self.triangulator.epsilon)
|
||||
if side == otherSide then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self:_hasAnyOnSideImpl(segment, side, parentPolygon, ...)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @param side -1 | 0 | 1
|
||||
--- @param parentPolygon slick.geometry.clipper.polygon
|
||||
--- @param childPolygons number[]
|
||||
--- @param ... number[]
|
||||
function clipper:_hasAnyOnSide(x1, y1, x2, y2, side, parentPolygon, childPolygons, ...)
|
||||
self.cachedSegment.a:init(x1, y1)
|
||||
self.cachedSegment.b:init(x2, y2)
|
||||
|
||||
return self:_hasAnyOnSideImpl(self.cachedSegment, side, parentPolygon, childPolygons, ...)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function clipper:_addPendingEdge(a, b)
|
||||
self.cachedEdge:init(a, b)
|
||||
local found = search.first(self.edges, self.cachedEdge, edge.compare)
|
||||
|
||||
if not found then
|
||||
table.insert(self.pendingPolygonEdges, a)
|
||||
table.insert(self.pendingPolygonEdges, b)
|
||||
|
||||
local e = self.edgesPool:allocate(a, b)
|
||||
table.insert(self.edges, search.lessThan(self.edges, e, edge.compare) + 1, e)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
function clipper:_popPendingEdge()
|
||||
local b = table.remove(self.pendingPolygonEdges)
|
||||
local a = table.remove(self.pendingPolygonEdges)
|
||||
|
||||
return a, b
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param a number?
|
||||
--- @param b number?
|
||||
function clipper:_addResultEdge(a, b)
|
||||
local aResultIndex = self.indexToResultIndex[a]
|
||||
if not aResultIndex and a then
|
||||
aResultIndex = self.resultIndex
|
||||
self.resultIndex = self.resultIndex + 1
|
||||
|
||||
self.indexToResultIndex[a] = aResultIndex
|
||||
|
||||
local j = (a - 1) * 2 + 1
|
||||
local k = j + 1
|
||||
|
||||
table.insert(self.resultPoints, self.resultPolygon.points[j])
|
||||
table.insert(self.resultPoints, self.resultPolygon.points[k])
|
||||
|
||||
if self.resultUserdata then
|
||||
self.resultUserdata[aResultIndex] = self.resultPolygon.userdata[a].userdata
|
||||
end
|
||||
end
|
||||
|
||||
local bResultIndex = self.indexToResultIndex[b]
|
||||
if not bResultIndex and b then
|
||||
bResultIndex = self.resultIndex
|
||||
self.resultIndex = self.resultIndex + 1
|
||||
|
||||
self.indexToResultIndex[b] = bResultIndex
|
||||
|
||||
local j = (b - 1) * 2 + 1
|
||||
local k = j + 1
|
||||
|
||||
table.insert(self.resultPoints, self.resultPolygon.points[j])
|
||||
table.insert(self.resultPoints, self.resultPolygon.points[k])
|
||||
|
||||
if self.resultUserdata then
|
||||
self.resultUserdata[bResultIndex] = self.resultPolygon.userdata[b].userdata
|
||||
end
|
||||
end
|
||||
|
||||
if a and b then
|
||||
table.insert(self.resultEdges, aResultIndex)
|
||||
table.insert(self.resultEdges, bResultIndex)
|
||||
|
||||
if self.resultExteriorEdges and (self.resultPolygon.userdata[a].isExteriorEdge or self.resultPolygon.userdata[b].isExteriorEdge) then
|
||||
table.insert(self.resultExteriorEdges, aResultIndex)
|
||||
table.insert(self.resultExteriorEdges, bResultIndex)
|
||||
end
|
||||
|
||||
if self.resultInteriorEdges and (self.resultPolygon.userdata[a].isInteriorEdge or self.resultPolygon.userdata[b].isInteriorEdge) then
|
||||
table.insert(self.resultInteriorEdges, aResultIndex)
|
||||
table.insert(self.resultInteriorEdges, bResultIndex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
function clipper:intersection(a, b)
|
||||
local aIndex = (a - 1) * 2 + 1
|
||||
local bIndex = (b - 1) * 2 + 1
|
||||
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local aUserdata = self.resultPolygon.userdata[a]
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local bUserdata = self.resultPolygon.userdata[b]
|
||||
|
||||
local aOtherPolygons = aUserdata.polygons[self.otherPolygon]
|
||||
local bOtherPolygons = bUserdata.polygons[self.otherPolygon]
|
||||
|
||||
local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1]
|
||||
local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1]
|
||||
|
||||
local abInsideSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon)
|
||||
local abInsideOther, abCollinearOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon)
|
||||
|
||||
local hasAnyCollinearOtherPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons)
|
||||
local hasAnyCollinearSubjectPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons)
|
||||
|
||||
if (abInsideOther and abInsideSubject) or (not abCollinearOther and ((abInsideOther and hasAnyCollinearSubjectPoints) or (abInsideSubject and hasAnyCollinearOtherPoints))) then
|
||||
self:_addResultEdge(a, b)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
function clipper:union(a, b)
|
||||
local aIndex = (a - 1) * 2 + 1
|
||||
local bIndex = (b - 1) * 2 + 1
|
||||
|
||||
local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1]
|
||||
local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1]
|
||||
|
||||
local abInsideSubject, abCollinearSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon)
|
||||
local abInsideOther, abCollinearOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon)
|
||||
|
||||
abInsideSubject = abInsideSubject or abCollinearSubject
|
||||
abInsideOther = abInsideOther or abCollinearOther
|
||||
|
||||
if (abInsideOther or abInsideSubject) and not (abInsideOther and abInsideSubject) then
|
||||
self:_addResultEdge(a, b)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
function clipper:difference(a, b)
|
||||
local aIndex = (a - 1) * 2 + 1
|
||||
local bIndex = (b - 1) * 2 + 1
|
||||
|
||||
local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1]
|
||||
local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1]
|
||||
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local aUserdata = self.resultPolygon.userdata[a]
|
||||
--- @type slick.geometry.clipper.polygonUserdata
|
||||
local bUserdata = self.resultPolygon.userdata[b]
|
||||
|
||||
local aOtherPolygons = aUserdata.polygons[self.otherPolygon]
|
||||
local bOtherPolygons = bUserdata.polygons[self.otherPolygon]
|
||||
|
||||
local hasAnyCollinearOtherPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons)
|
||||
|
||||
local abInsideSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon)
|
||||
local abInsideOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon)
|
||||
|
||||
if abInsideSubject and (not abInsideOther or hasAnyCollinearOtherPoints) then
|
||||
self:_addResultEdge(a, b)
|
||||
end
|
||||
end
|
||||
|
||||
--- @alias slick.geometry.clipper.mergeFunction fun(combine: slick.geometry.clipper.merge)
|
||||
--- @class slick.geometry.clipper.clipOptions : slick.geometry.triangulation.delaunayCleanupOptions
|
||||
--- @field merge slick.geometry.clipper.mergeFunction?
|
||||
local clipOptions = {}
|
||||
|
||||
--- @param operation slick.geometry.clipper.clipOperation
|
||||
--- @param subjectPoints number[]
|
||||
--- @param subjectEdges number[] | number[][]
|
||||
--- @param otherPoints number[]
|
||||
--- @param otherEdges number[] | number[][]
|
||||
--- @param options slick.geometry.clipper.clipOptions?
|
||||
--- @param subjectUserdata any[]?
|
||||
--- @param otherUserdata any[]?
|
||||
--- @param resultPoints number[]?
|
||||
--- @param resultEdges number[]?
|
||||
--- @param resultUserdata any[]?
|
||||
--- @param resultExteriorEdges number[]?
|
||||
--- @param resultInteriorEdges number[]?
|
||||
function clipper:clip(operation, subjectPoints, subjectEdges, otherPoints, otherEdges, options, subjectUserdata, otherUserdata, resultPoints, resultEdges, resultUserdata, resultExteriorEdges, resultInteriorEdges)
|
||||
self:reset()
|
||||
|
||||
if type(subjectEdges) == "table" and #subjectEdges >= 1 and type(subjectEdges[1]) == "table" then
|
||||
--- @cast subjectEdges number[][]
|
||||
self:_addPolygon(subjectPoints, subjectEdges[1], subjectEdges[2], subjectUserdata, self.subjectPolygon)
|
||||
else
|
||||
self:_addPolygon(subjectPoints, subjectEdges, nil, subjectUserdata, self.subjectPolygon)
|
||||
end
|
||||
|
||||
if type(otherEdges) == "table" and #otherEdges >= 1 and type(otherEdges[1]) == "table" then
|
||||
--- @cast otherEdges number[][]
|
||||
self:_addPolygon(otherPoints, otherEdges[1], otherEdges[2], otherUserdata, self.otherPolygon)
|
||||
else
|
||||
self:_addPolygon(otherPoints, otherEdges, nil, otherUserdata, self.otherPolygon)
|
||||
end
|
||||
|
||||
self:_preparePolygon(subjectPoints, self.subjectPolygon)
|
||||
self:_preparePolygon(otherPoints, self.otherPolygon)
|
||||
|
||||
self.inputCleanupOptions = options
|
||||
self.triangulator:clean(self.combinedPoints, self.combinedEdges, self.combinedUserdata, self.clipCleanupOptions, self.resultPolygon.points, self.resultPolygon.edges, self.resultPolygon.userdata)
|
||||
|
||||
resultPoints = resultPoints or {}
|
||||
resultEdges = resultEdges or {}
|
||||
resultUserdata = (subjectUserdata and otherUserdata) and resultUserdata or {}
|
||||
|
||||
self.resultPoints = resultPoints
|
||||
self.resultEdges = resultEdges
|
||||
self.resultUserdata = resultUserdata
|
||||
self.resultPoints = resultPoints
|
||||
self.resultInteriorEdges = resultInteriorEdges
|
||||
self.resultExteriorEdges = resultExteriorEdges
|
||||
|
||||
slicktable.clear(resultPoints)
|
||||
slicktable.clear(resultEdges)
|
||||
if resultUserdata then
|
||||
slicktable.clear(resultUserdata)
|
||||
end
|
||||
if resultInteriorEdges then
|
||||
slicktable.clear(resultInteriorEdges)
|
||||
end
|
||||
if resultExteriorEdges then
|
||||
slicktable.clear(resultExteriorEdges)
|
||||
end
|
||||
|
||||
for i = 1, #self.resultPolygon.edges, 2 do
|
||||
local a = self.resultPolygon.edges[i]
|
||||
local b = self.resultPolygon.edges[i + 1]
|
||||
|
||||
operation(self, a, b)
|
||||
end
|
||||
|
||||
self:_mergePoints(operation)
|
||||
self:_mergeUserdata()
|
||||
|
||||
self.resultPoints = nil
|
||||
self.resultEdges = nil
|
||||
self.resultUserdata = nil
|
||||
|
||||
for i = 1, #self.combinedUserdata do
|
||||
-- Don't leak user-provided resources.
|
||||
self.combinedUserdata[i].userdata = nil
|
||||
end
|
||||
|
||||
return resultPoints, resultEdges, resultUserdata, resultExteriorEdges, resultInteriorEdges
|
||||
end
|
||||
|
||||
return clipper
|
||||
14
game/love_src/lib/slick/geometry/init.lua
Normal file
14
game/love_src/lib/slick/geometry/init.lua
Normal file
@ -0,0 +1,14 @@
|
||||
--- @alias slick.geometry.shape slick.geometry.point | slick.geometry.ray | slick.geometry.rectangle | slick.geometry.segment
|
||||
|
||||
local geometry = {
|
||||
clipper = require("slick.geometry.clipper"),
|
||||
triangulation = require("slick.geometry.triangulation"),
|
||||
point = require("slick.geometry.point"),
|
||||
ray = require("slick.geometry.ray"),
|
||||
rectangle = require("slick.geometry.rectangle"),
|
||||
segment = require("slick.geometry.segment"),
|
||||
simple = require("slick.geometry.simple"),
|
||||
transform = require("slick.geometry.transform"),
|
||||
}
|
||||
|
||||
return geometry
|
||||
42
game/love_src/lib/slick/geometry/merge.lua
Normal file
42
game/love_src/lib/slick/geometry/merge.lua
Normal file
@ -0,0 +1,42 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.geometry.clipper.merge
|
||||
--- @field source "subject" | "other"
|
||||
--- @field target "subject" | "other"
|
||||
--- @field sourceIndex number
|
||||
--- @field sourceUserdata any
|
||||
--- @field resultIndex number
|
||||
--- @field resultUserdata any
|
||||
local merge = {}
|
||||
local metatable = { __index = merge }
|
||||
|
||||
--- @return slick.geometry.clipper.merge
|
||||
function merge.new()
|
||||
return setmetatable({}, metatable)
|
||||
end
|
||||
|
||||
--- @param source "subject" | "other"
|
||||
--- @param sourceIndex number
|
||||
--- @param sourceUserdata any
|
||||
--- @param resultIndex number
|
||||
function merge:init(source, sourceIndex, sourceUserdata, resultIndex)
|
||||
self.source = source
|
||||
if source == "subject" then
|
||||
self.target = "other"
|
||||
else
|
||||
self.target = "subject"
|
||||
end
|
||||
|
||||
self.sourceIndex = sourceIndex
|
||||
self.sourceUserdata = sourceUserdata
|
||||
self.resultIndex = resultIndex
|
||||
self.resultUserdata = nil
|
||||
end
|
||||
|
||||
--- @param m slick.geometry.clipper.merge
|
||||
function merge.default(m)
|
||||
-- No-op.
|
||||
end
|
||||
|
||||
return merge
|
||||
233
game/love_src/lib/slick/geometry/point.lua
Normal file
233
game/love_src/lib/slick/geometry/point.lua
Normal file
@ -0,0 +1,233 @@
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.geometry.point
|
||||
--- @field x number
|
||||
--- @field y number
|
||||
local point = {}
|
||||
local metatable = {
|
||||
__index = point,
|
||||
__tostring = function(self)
|
||||
return string.format("slick.geometry.point (x = %.2f, y = %.2f)", self.x, self.y)
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
--- @param x number?
|
||||
--- @param y number?
|
||||
--- @return slick.geometry.point
|
||||
function point.new(x, y)
|
||||
return setmetatable({ x = x or 0, y = y or 0 }, metatable)
|
||||
end
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function point:init(x, y)
|
||||
self.x = x
|
||||
self.y = y
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @return slick.util.search.compareResult
|
||||
function point.compare(a, b, E)
|
||||
local result = slickmath.sign(a.x - b.x, E or slickmath.EPSILON)
|
||||
if result ~= 0 then
|
||||
return result
|
||||
end
|
||||
|
||||
return slickmath.sign(a.y - b.y, E or slickmath.EPSILON)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @return boolean
|
||||
function point.less(a, b)
|
||||
return point.compare(a, b) < 0
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return slick.geometry.point
|
||||
function point:higher(other)
|
||||
if self:greaterThan(other) then
|
||||
return self
|
||||
end
|
||||
|
||||
return other
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return slick.geometry.point
|
||||
function point:lower(other)
|
||||
if self:lessThan(other) then
|
||||
return self
|
||||
end
|
||||
|
||||
return other
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:equal(other)
|
||||
return self.x == other.x and self.y == other.y
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:notEqual(other)
|
||||
return not point:equal(other)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:greaterThan(other)
|
||||
return self.x > other.x or (self.x == other.x and self.y > other.y)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:greaterThanEqual(other)
|
||||
return self:greaterThan(other) or self:equal(other)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:lessThan(other)
|
||||
return self.x < other.x or (self.x == other.x and self.y < other.y)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return boolean
|
||||
function point:lessThanOrEqual(other)
|
||||
return self:lessThan(other) or self:equal(other)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
function point:direction(other, result)
|
||||
result:init(other.x - self.x, other.y - self.y)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
function point:left(other)
|
||||
other:init(self.y, -self.x)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
function point:right(other)
|
||||
other:init(-self.y, self.x)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return number
|
||||
function point:dot(other)
|
||||
return self.x * other.x + self.y * other.y
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
function point:add(other, result)
|
||||
result.x = self.x + other.x
|
||||
result.y = self.y + other.y
|
||||
end
|
||||
|
||||
--- @param other number
|
||||
--- @param result slick.geometry.point
|
||||
function point:addScalar(other, result)
|
||||
result.x = self.x + other
|
||||
result.y = self.y + other
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
function point:sub(other, result)
|
||||
result.x = self.x - other.x
|
||||
result.y = self.y - other.y
|
||||
end
|
||||
|
||||
--- @param other number
|
||||
--- @param result slick.geometry.point
|
||||
function point:subScalar(other, result)
|
||||
result.x = self.x - other
|
||||
result.y = self.y - other
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
function point:multiply(other, result)
|
||||
result.x = self.x * other.x
|
||||
result.y = self.y * other.y
|
||||
end
|
||||
|
||||
--- @param other number
|
||||
--- @param result slick.geometry.point
|
||||
function point:multiplyScalar(other, result)
|
||||
result.x = self.x * other
|
||||
result.y = self.y * other
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
function point:divide(other, result)
|
||||
result.x = self.x / other.x
|
||||
result.y = self.y / other.y
|
||||
end
|
||||
|
||||
--- @param other number
|
||||
--- @param result slick.geometry.point
|
||||
function point:divideScalar(other, result)
|
||||
result.x = self.x / other
|
||||
result.y = self.y / other
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function point:lengthSquared()
|
||||
return self.x ^ 2 + self.y ^ 2
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function point:length()
|
||||
return math.sqrt(self:lengthSquared())
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return number
|
||||
function point:distanceSquared(other)
|
||||
return (self.x - other.x) ^ 2 + (self.y - other.y) ^ 2
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @return number
|
||||
function point:distance(other)
|
||||
return math.sqrt(self:distanceSquared(other))
|
||||
end
|
||||
|
||||
--- @param result slick.geometry.point
|
||||
function point:normalize(result)
|
||||
local length = self:length()
|
||||
if length > 0 then
|
||||
result.x = self.x / length
|
||||
result.y = self.y / length
|
||||
end
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.point
|
||||
--- @param E number
|
||||
function point:round(other, E)
|
||||
other.x = self.x
|
||||
if other.x > -E and other.x < E then
|
||||
other.x = 0
|
||||
end
|
||||
|
||||
other.y = self.y
|
||||
if other.y > -E and other.y < E then
|
||||
other.y = 0
|
||||
end
|
||||
end
|
||||
|
||||
--- @param result slick.geometry.point
|
||||
function point:negate(result)
|
||||
result.x = -self.x
|
||||
result.y = -self.y
|
||||
end
|
||||
|
||||
return point
|
||||
122
game/love_src/lib/slick/geometry/ray.lua
Normal file
122
game/love_src/lib/slick/geometry/ray.lua
Normal file
@ -0,0 +1,122 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.geometry.ray
|
||||
--- @field origin slick.geometry.point
|
||||
--- @field direction slick.geometry.point
|
||||
local ray = {}
|
||||
local metatable = { __index = ray }
|
||||
|
||||
--- @param origin slick.geometry.point?
|
||||
--- @param direction slick.geometry.point?
|
||||
--- @return slick.geometry.ray
|
||||
function ray.new(origin, direction)
|
||||
local result = setmetatable({
|
||||
origin = point.new(origin and origin.x, origin and origin.y),
|
||||
direction = point.new(direction and direction.x, direction and direction.y),
|
||||
}, metatable)
|
||||
|
||||
if result.direction:lengthSquared() > 0 then
|
||||
result.direction:normalize(result.direction)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @param origin slick.geometry.point
|
||||
--- @param direction slick.geometry.point
|
||||
function ray:init(origin, direction)
|
||||
self.origin:init(origin.x, origin.y)
|
||||
self.direction:init(direction.x, direction.y)
|
||||
self.direction:normalize(self.direction)
|
||||
end
|
||||
|
||||
--- @param distance number
|
||||
--- @param result slick.geometry.point
|
||||
function ray:project(distance, result)
|
||||
result:init(self.direction.x, self.direction.y)
|
||||
result:multiplyScalar(distance, result)
|
||||
self.origin:add(result, result)
|
||||
end
|
||||
|
||||
local _cachedHitSegmentPointA = point.new()
|
||||
local _cachedHitSegmentPointB = point.new()
|
||||
local _cachedHitSegmentPointC = point.new()
|
||||
local _cachedHitSegmentPointD = point.new()
|
||||
local _cachedHitSegmentResult = point.new()
|
||||
local _cachedHitSegmentDirection = point.new()
|
||||
|
||||
--- @param s slick.geometry.segment
|
||||
--- @param E number?
|
||||
--- @return boolean, number?, number?, number?
|
||||
function ray:hitSegment(s, E)
|
||||
E = E or 0
|
||||
|
||||
_cachedHitSegmentPointA:init(s.a.x, s.a.y)
|
||||
_cachedHitSegmentPointB:init(s.b.x, s.b.y)
|
||||
|
||||
_cachedHitSegmentPointC:init(self.origin.x, self.origin.y)
|
||||
self.origin:add(self.direction, _cachedHitSegmentPointD)
|
||||
|
||||
local bax = _cachedHitSegmentPointB.x - _cachedHitSegmentPointA.x
|
||||
local bay = _cachedHitSegmentPointB.y - _cachedHitSegmentPointA.y
|
||||
local dcx = _cachedHitSegmentPointD.x - _cachedHitSegmentPointC.x
|
||||
local dcy = _cachedHitSegmentPointD.y - _cachedHitSegmentPointC.y
|
||||
|
||||
local baCrossDC = bax * dcy - bay * dcx
|
||||
local dcCrossBA = dcx * bay - dcy * bax
|
||||
if baCrossDC == 0 or dcCrossBA == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local acx = _cachedHitSegmentPointA.x - _cachedHitSegmentPointC.x
|
||||
local acy = _cachedHitSegmentPointA.y - _cachedHitSegmentPointC.y
|
||||
|
||||
local dcCrossAC = dcx * acy - dcy * acx
|
||||
|
||||
local u = dcCrossAC / baCrossDC
|
||||
if u < -E or u > (1 + E) then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local rx = _cachedHitSegmentPointA.x + bax * u
|
||||
local ry = _cachedHitSegmentPointA.y + bay * u
|
||||
|
||||
_cachedHitSegmentResult:init(rx, ry)
|
||||
self.origin:direction(_cachedHitSegmentResult, _cachedHitSegmentDirection)
|
||||
if _cachedHitSegmentDirection:dot(self.direction) < 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
return true, rx, ry, u
|
||||
end
|
||||
|
||||
--- @param r slick.geometry.rectangle
|
||||
--- @return boolean, number?, number?
|
||||
function ray:hitRectangle(r)
|
||||
-- https://tavianator.com/fast-branchless-raybounding-box-intersections/
|
||||
local inverseDirectionX = 1 / self.direction.x
|
||||
local inverseDirectionY = 1 / self.direction.y
|
||||
local tMin, tMax
|
||||
|
||||
local tx1 = (r:left() - self.origin.x) * inverseDirectionX
|
||||
local tx2 = (r:right() - self.origin.x) * inverseDirectionX
|
||||
|
||||
tMin = math.min(tx1, tx2)
|
||||
tMax = math.max(tx1, tx2)
|
||||
|
||||
local ty1 = (r:top() - self.origin.y) * inverseDirectionY
|
||||
local ty2 = (r:bottom() - self.origin.y) * inverseDirectionY
|
||||
|
||||
tMin = math.max(tMin, math.min(ty1, ty2))
|
||||
tMax = math.min(tMax, math.max(ty1, ty2))
|
||||
|
||||
if tMax >= tMin then
|
||||
return true, self.origin.x + self.direction.x * tMin, self.origin.y + self.direction.y * tMin
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return ray
|
||||
102
game/love_src/lib/slick/geometry/rectangle.lua
Normal file
102
game/love_src/lib/slick/geometry/rectangle.lua
Normal file
@ -0,0 +1,102 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.geometry.rectangle
|
||||
--- @field topLeft slick.geometry.point
|
||||
--- @field bottomRight slick.geometry.point
|
||||
local rectangle = {}
|
||||
local metatable = {
|
||||
__index = rectangle
|
||||
}
|
||||
|
||||
--- @param x1 number?
|
||||
--- @param y1 number?
|
||||
--- @param x2 number?
|
||||
--- @param y2 number?
|
||||
--- @return slick.geometry.rectangle
|
||||
function rectangle.new(x1, y1, x2, y2)
|
||||
local result = setmetatable({ topLeft = point.new(), bottomRight = point.new() }, metatable)
|
||||
result:init(x1, y1, x2, y2)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @param x1 number?
|
||||
--- @param y1 number?
|
||||
--- @param x2 number?
|
||||
--- @param y2 number?
|
||||
function rectangle:init(x1, y1, x2, y2)
|
||||
x1 = x1 or 0
|
||||
x2 = x2 or x1
|
||||
y1 = y1 or 0
|
||||
y2 = y2 or y1
|
||||
|
||||
self.topLeft:init(math.min(x1, x2), math.min(y1, y2))
|
||||
self.bottomRight:init(math.max(x1, x2), math.max(y1, y2))
|
||||
end
|
||||
|
||||
function rectangle:left()
|
||||
return self.topLeft.x
|
||||
end
|
||||
|
||||
function rectangle:right()
|
||||
return self.bottomRight.x
|
||||
end
|
||||
|
||||
function rectangle:top()
|
||||
return self.topLeft.y
|
||||
end
|
||||
|
||||
function rectangle:bottom()
|
||||
return self.bottomRight.y
|
||||
end
|
||||
|
||||
function rectangle:width()
|
||||
return self:right() - self:left()
|
||||
end
|
||||
|
||||
function rectangle:height()
|
||||
return self:bottom() - self:top()
|
||||
end
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function rectangle:expand(x, y)
|
||||
self.topLeft.x = math.min(self.topLeft.x, x)
|
||||
self.topLeft.y = math.min(self.topLeft.y, y)
|
||||
self.bottomRight.x = math.max(self.bottomRight.x, x)
|
||||
self.bottomRight.y = math.max(self.bottomRight.y, y)
|
||||
end
|
||||
|
||||
---@param x number
|
||||
---@param y number
|
||||
function rectangle:move(x, y)
|
||||
self.topLeft.x = self.topLeft.x + x
|
||||
self.topLeft.y = self.topLeft.y + y
|
||||
self.bottomRight.x = self.bottomRight.x + x
|
||||
self.bottomRight.y = self.bottomRight.y + y
|
||||
end
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function rectangle:sweep(x, y)
|
||||
self:expand(x - self:width(), y - self:height())
|
||||
self:expand(x + self:width(), y + self:height())
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.rectangle
|
||||
--- @return boolean
|
||||
function rectangle:overlaps(other)
|
||||
return self:left() <= other:right() and self:right() >= other:left() and
|
||||
self:top() <= other:bottom() and self:bottom() >= other:top()
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @param E number?
|
||||
--- @return boolean
|
||||
function rectangle:inside(p, E)
|
||||
E = E or 0
|
||||
return slickmath.withinRange(p.x, self:left(), self:right(), E) and slickmath.withinRange(p.y, self:top(), self:bottom(), E)
|
||||
end
|
||||
|
||||
return rectangle
|
||||
211
game/love_src/lib/slick/geometry/segment.lua
Normal file
211
game/love_src/lib/slick/geometry/segment.lua
Normal file
@ -0,0 +1,211 @@
|
||||
local point = require("slick.geometry.point")
|
||||
|
||||
--- @class slick.geometry.segment
|
||||
--- @field a slick.geometry.point
|
||||
--- @field b slick.geometry.point
|
||||
local segment = {}
|
||||
local metatable = { __index = segment }
|
||||
|
||||
--- @param a slick.geometry.point?
|
||||
--- @param b slick.geometry.point?
|
||||
--- @return slick.geometry.segment
|
||||
function segment.new(a, b)
|
||||
return setmetatable({
|
||||
a = point.new(a and a.x, a and a.y),
|
||||
b = point.new(b and b.x, b and b.y),
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
function segment:init(a, b)
|
||||
self.a:init(a.x, a.y)
|
||||
self.b:init(b.x, b.y)
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function segment:left()
|
||||
return math.min(self.a.x, self.b.x)
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function segment:right()
|
||||
return math.max(self.a.x, self.b.x)
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function segment:top()
|
||||
return math.min(self.a.y, self.b.y)
|
||||
end
|
||||
|
||||
--- @return number
|
||||
function segment:bottom()
|
||||
return math.max(self.a.y, self.b.y)
|
||||
end
|
||||
|
||||
--- @param delta number
|
||||
--- @param result slick.geometry.point
|
||||
function segment:lerp(delta, result)
|
||||
result.x = self.b.x * delta + self.a.x * (1 - delta)
|
||||
result.y = self.b.y * delta + self.a.y * (1 - delta)
|
||||
end
|
||||
|
||||
local _cachedProjectionBMinusA = point.new()
|
||||
local _cachedProjectionPMinusA = point.new()
|
||||
|
||||
--- Unlike `project`, this treats the line segment as a line.
|
||||
--- @param p slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
--- @return number
|
||||
function segment:projectLine(p, result)
|
||||
local distanceSquared = self.a:distanceSquared(self.b)
|
||||
if distanceSquared == 0 then
|
||||
result:init(self.a.x, self.a.y)
|
||||
return 0
|
||||
end
|
||||
|
||||
p:sub(self.a, _cachedProjectionPMinusA)
|
||||
self.b:sub(self.a, _cachedProjectionBMinusA)
|
||||
|
||||
local t = _cachedProjectionPMinusA:dot(_cachedProjectionBMinusA) / distanceSquared
|
||||
|
||||
_cachedProjectionBMinusA:multiplyScalar(t, result)
|
||||
self.a:add(result, result)
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @param result slick.geometry.point
|
||||
--- @return number
|
||||
function segment:project(p, result)
|
||||
local distanceSquared = self.a:distanceSquared(self.b)
|
||||
if distanceSquared == 0 then
|
||||
result:init(self.a.x, self.a.y)
|
||||
return 0
|
||||
end
|
||||
|
||||
p:sub(self.a, _cachedProjectionPMinusA)
|
||||
self.b:sub(self.a, _cachedProjectionBMinusA)
|
||||
|
||||
local t = math.max(0, math.min(1, _cachedProjectionPMinusA:dot(_cachedProjectionBMinusA) / distanceSquared))
|
||||
|
||||
_cachedProjectionBMinusA:multiplyScalar(t, result)
|
||||
self.a:add(result, result)
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
local _cachedDistanceProjectedAB = point.new()
|
||||
--- @param p slick.geometry.point
|
||||
function segment:distanceSquared(p)
|
||||
self:project(p, _cachedDistanceProjectedAB)
|
||||
return _cachedDistanceProjectedAB:distanceSquared(p)
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
function segment:distance(p)
|
||||
return math.sqrt(self:distanceSquared(p))
|
||||
end
|
||||
|
||||
--- @alias slick.geometry.segmentCompareFunc fun(a: slick.geometry.segment, b: slick.geometry.segment): slick.util.search.compareResult
|
||||
|
||||
--- @param a slick.geometry.segment
|
||||
--- @param b slick.geometry.segment
|
||||
--- @param E number?
|
||||
--- @return slick.util.search.compareResult
|
||||
function segment.compare(a, b, E)
|
||||
local aMinPoint, aMaxPoint
|
||||
if a.b:lessThan(a.a) then
|
||||
aMinPoint = a.b
|
||||
aMaxPoint = a.a
|
||||
else
|
||||
aMinPoint = a.a
|
||||
aMaxPoint = a.b
|
||||
end
|
||||
|
||||
local bMinPoint, bMaxPoint
|
||||
if b.b:lessThan(b.a) then
|
||||
bMinPoint = b.b
|
||||
bMaxPoint = b.a
|
||||
else
|
||||
bMinPoint = b.a
|
||||
bMaxPoint = b.b
|
||||
end
|
||||
|
||||
local s = point.compare(aMinPoint, bMinPoint, E)
|
||||
if s ~= 0 then
|
||||
return s
|
||||
end
|
||||
|
||||
return point.compare(aMaxPoint, bMaxPoint, E)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.segment
|
||||
--- @param b slick.geometry.segment
|
||||
--- @return boolean
|
||||
function segment.less(a, b)
|
||||
return segment.compare(a, b) < 0
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.segment
|
||||
--- @return boolean
|
||||
function segment:lessThan(other)
|
||||
return self.a:lessThan(other.a) or
|
||||
(self.a:equal(other.a) and self.b:lessThan(other.b))
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.segment
|
||||
--- @return boolean
|
||||
function segment:overlap(other)
|
||||
local selfLeft = math.min(self.a.x, self.b.x)
|
||||
local selfRight = math.max(self.a.x, self.b.x)
|
||||
local selfTop = math.min(self.a.y, self.b.y)
|
||||
local selfBottom = math.max(self.a.y, self.b.y)
|
||||
|
||||
local otherLeft = math.min(other.a.x, other.b.x)
|
||||
local otherRight = math.max(other.a.x, other.b.x)
|
||||
local otherTop = math.min(other.a.y, other.b.y)
|
||||
local otherBottom = math.max(other.a.y, other.b.y)
|
||||
|
||||
return (selfLeft <= otherRight and selfRight >= otherLeft) and
|
||||
(selfTop <= otherBottom and selfBottom >= otherTop)
|
||||
end
|
||||
|
||||
--- @param other slick.geometry.segment
|
||||
--- @param E number?
|
||||
--- @return boolean
|
||||
--- @return number?
|
||||
--- @return number?
|
||||
--- @return number?
|
||||
function segment:intersection(other, E)
|
||||
E = E or 0
|
||||
|
||||
local bax = self.b.x - self.a.x
|
||||
local bay = self.b.y - self.a.y
|
||||
local dcx = other.b.x - other.a.x
|
||||
local dcy = other.b.y - other.a.y
|
||||
|
||||
local baCrossDC = bax * dcy - bay * dcx
|
||||
local dcCrossBA = dcx * bay - dcy * bax
|
||||
if baCrossDC == 0 or dcCrossBA == 0 then
|
||||
return false, nil, nil, nil
|
||||
end
|
||||
|
||||
local acx = self.a.x - other.a.x
|
||||
local acy = self.a.y - other.a.y
|
||||
|
||||
local dcCrossAC = dcx * acy - dcy * acx
|
||||
|
||||
local u = dcCrossAC / baCrossDC
|
||||
if u < -E or u > (1 + E) then
|
||||
return false
|
||||
end
|
||||
|
||||
local rx = self.a.x + bax * u
|
||||
local ry = self.a.y + bay * u
|
||||
|
||||
return true, rx, ry, u
|
||||
end
|
||||
|
||||
return segment
|
||||
186
game/love_src/lib/slick/geometry/simple.lua
Normal file
186
game/love_src/lib/slick/geometry/simple.lua
Normal file
@ -0,0 +1,186 @@
|
||||
local clipper = require "slick.geometry.clipper"
|
||||
local delaunay = require "slick.geometry.triangulation.delaunay"
|
||||
local util = require "slick.util"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
|
||||
local simple = {}
|
||||
|
||||
--- @param contours number[][]
|
||||
--- @return number[], number[]
|
||||
local function _getPointEdges(contours)
|
||||
local points = {}
|
||||
local edges = {}
|
||||
|
||||
for _, contour in ipairs(contours) do
|
||||
local numPoints = #points
|
||||
for j = 1, #contour, 2 do
|
||||
table.insert(points, contour[j])
|
||||
table.insert(points, contour[j + 1])
|
||||
|
||||
table.insert(edges, (numPoints / 2) + (j + 1) / 2)
|
||||
table.insert(edges, (numPoints / 2) + (slickmath.wrap(j, 2, #contour) + 1) / 2)
|
||||
end
|
||||
end
|
||||
|
||||
return points, edges
|
||||
end
|
||||
|
||||
--- @param points number[]
|
||||
--- @param polygons number[][]?
|
||||
--- @return number[][]
|
||||
local function _getPolygons(points, polygons)
|
||||
local result = {}
|
||||
|
||||
if not polygons then
|
||||
return result
|
||||
end
|
||||
|
||||
for _, polygon in ipairs(polygons) do
|
||||
local resultPolygon = {}
|
||||
for _, vertex in ipairs(polygon) do
|
||||
local i = (vertex - 1) * 2 + 1
|
||||
|
||||
table.insert(resultPolygon, points[i])
|
||||
table.insert(resultPolygon, points[i + 1])
|
||||
end
|
||||
table.insert(result, resultPolygon)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local triangulateOptions = {
|
||||
refine = true,
|
||||
interior = true,
|
||||
exterior = false,
|
||||
polygonization = false
|
||||
}
|
||||
|
||||
--- @param contours number[][]
|
||||
--- @return number[][]
|
||||
function simple.triangulate(contours)
|
||||
local points, edges = _getPointEdges(contours)
|
||||
|
||||
local triangulator = delaunay.new()
|
||||
local cleanPoints, cleanEdges = triangulator:clean(points, edges)
|
||||
local triangles = triangulator:triangulate(cleanPoints, cleanEdges, triangulateOptions)
|
||||
|
||||
return _getPolygons(cleanPoints, triangles)
|
||||
end
|
||||
|
||||
local polygonizeOptions = {
|
||||
refine = true,
|
||||
interior = true,
|
||||
exterior = false,
|
||||
polygonization = true,
|
||||
maxPolygonVertexCount = math.huge
|
||||
}
|
||||
|
||||
--- @overload fun(n: number, contours: number[][]): number[][]
|
||||
--- @overload fun(contours: number[][]): number[][]
|
||||
--- @return number[][]
|
||||
function simple.polygonize(n, b)
|
||||
local points, edges
|
||||
if type(n) == "number" then
|
||||
polygonizeOptions.maxPolygonVertexCount = math.max(n, 3)
|
||||
points, edges = _getPointEdges(b)
|
||||
else
|
||||
polygonizeOptions.maxPolygonVertexCount = math.huge
|
||||
points, edges = _getPointEdges(n)
|
||||
end
|
||||
|
||||
local triangulator = delaunay.new()
|
||||
local cleanPoints, cleanEdges = triangulator:clean(points, edges)
|
||||
local _, _, polygons = triangulator:triangulate(cleanPoints, cleanEdges, polygonizeOptions)
|
||||
|
||||
return _getPolygons(cleanPoints, polygons)
|
||||
end
|
||||
|
||||
--- @class slick.simple.clipOperation
|
||||
--- @field operation slick.geometry.clipper.clipOperation
|
||||
--- @field subject number[][] | slick.simple.clipOperation
|
||||
--- @field other number[][] | slick.simple.clipOperation
|
||||
local clipOperation = {}
|
||||
local clipOperationMetatable = { __index = clipOperation }
|
||||
|
||||
--- @package
|
||||
--- @param clipper slick.geometry.clipper
|
||||
function clipOperation:perform(clipper)
|
||||
local subjectPoints, subjectEdges
|
||||
if util.is(self.subject, clipOperation) then
|
||||
subjectPoints, subjectEdges = self.subject:perform(clipper)
|
||||
else
|
||||
subjectPoints, subjectEdges = _getPointEdges(self.subject)
|
||||
end
|
||||
|
||||
local otherPoints, otherEdges
|
||||
if util.is(self.other, clipOperation) then
|
||||
otherPoints, otherEdges = self.other:perform(clipper)
|
||||
else
|
||||
otherPoints, otherEdges = _getPointEdges(self.other)
|
||||
end
|
||||
|
||||
return clipper:clip(self.operation, subjectPoints, subjectEdges, otherPoints, otherEdges)
|
||||
end
|
||||
|
||||
--- @param subject number[][] | slick.simple.clipOperation
|
||||
--- @param other number[][] | slick.simple.clipOperation
|
||||
--- @return slick.simple.clipOperation
|
||||
function simple.newUnionClipOperation(subject, other)
|
||||
return setmetatable({
|
||||
operation = clipper.union,
|
||||
subject = subject,
|
||||
other = other
|
||||
}, clipOperationMetatable)
|
||||
end
|
||||
|
||||
--- @param subject number[][] | slick.simple.clipOperation
|
||||
--- @param other number[][] | slick.simple.clipOperation
|
||||
--- @return slick.simple.clipOperation
|
||||
function simple.newDifferenceClipOperation(subject, other)
|
||||
return setmetatable({
|
||||
operation = clipper.difference,
|
||||
subject = subject,
|
||||
other = other
|
||||
}, clipOperationMetatable)
|
||||
end
|
||||
|
||||
--- @param subject number[][] | slick.simple.clipOperation
|
||||
--- @param other number[][] | slick.simple.clipOperation
|
||||
--- @return slick.simple.clipOperation
|
||||
function simple.newIntersectionClipOperation(subject, other)
|
||||
return setmetatable({
|
||||
operation = clipper.intersection,
|
||||
subject = subject,
|
||||
other = other
|
||||
}, clipOperationMetatable)
|
||||
end
|
||||
|
||||
|
||||
--- @param operation slick.simple.clipOperation
|
||||
--- @param maxVertexCount number?
|
||||
--- @return number[][]
|
||||
function simple.clip(operation, maxVertexCount)
|
||||
maxVertexCount = math.max(maxVertexCount or 3, 3)
|
||||
|
||||
assert(util.is(operation, clipOperation))
|
||||
|
||||
local triangulator = delaunay.new()
|
||||
local c = clipper.new(triangulator)
|
||||
|
||||
local clippedPoints, clippedEdges = operation:perform(c)
|
||||
|
||||
local result
|
||||
if maxVertexCount == 3 then
|
||||
local triangles = triangulator:triangulate(clippedPoints, clippedEdges, triangulateOptions)
|
||||
result = triangles
|
||||
else
|
||||
polygonizeOptions.maxPolygonVertexCount = maxVertexCount
|
||||
local _, _, polygons = triangulator:triangulate(clippedPoints, clippedEdges, polygonizeOptions)
|
||||
result = polygons
|
||||
end
|
||||
|
||||
return _getPolygons(clippedPoints, result)
|
||||
end
|
||||
|
||||
return simple
|
||||
152
game/love_src/lib/slick/geometry/transform.lua
Normal file
152
game/love_src/lib/slick/geometry/transform.lua
Normal file
@ -0,0 +1,152 @@
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- Represents a transform.
|
||||
--- @class slick.geometry.transform
|
||||
--- @field private immutable boolean
|
||||
--- @field x number
|
||||
--- @field y number
|
||||
--- @field rotation number
|
||||
--- @field private rotationCos number
|
||||
--- @field private rotationSin number
|
||||
--- @field scaleX number
|
||||
--- @field scaleY number
|
||||
--- @field offsetX number
|
||||
--- @field offsetY number
|
||||
local transform = {}
|
||||
local metatable = { __index = transform }
|
||||
|
||||
--- Constructs a new transform.
|
||||
--- @param x number? translation on the x axis (defaults to 0)
|
||||
--- @param y number? translation on the y axis (defaults to 0)
|
||||
--- @param rotation number? rotation in radians (defaults to 0)
|
||||
--- @param scaleX number? scale along the x axis (defaults to 1)
|
||||
--- @param scaleY number? scale along the y axis (defaults to 1)
|
||||
--- @param offsetX number? offset along the x axis (defaults to 0)
|
||||
--- @param offsetY number? offsete along the y axis (defaults to 0)
|
||||
function transform.new(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
local result = setmetatable({}, metatable)
|
||||
result:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
result.immutable = false
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @package
|
||||
--- @param x number? translation on the x axis (defaults to 0)
|
||||
--- @param y number? translation on the y axis (defaults to 0)
|
||||
--- @param rotation number? rotation in radians (defaults to 0)
|
||||
--- @param scaleX number? scale along the x axis (defaults to 1)
|
||||
--- @param scaleY number? scale along the y axis (defaults to 1)
|
||||
--- @param offsetX number? offset along the x axis (defaults to 0)
|
||||
--- @param offsetY number? offsete along the y axis (defaults to 0)
|
||||
function transform._newImmutable(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
local result = setmetatable({}, metatable)
|
||||
result:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
result.immutable = true
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Same as setTransform.
|
||||
--- @param x number? translation on the x axis
|
||||
--- @param y number? translation on the y axis
|
||||
--- @param rotation number? rotation in radians
|
||||
--- @param scaleX number? scale along the x axis
|
||||
--- @param scaleY number? scale along the y axis
|
||||
--- @param offsetX number? offset along the x axis
|
||||
--- @param offsetY number? offsete along the y axis
|
||||
--- @see slick.geometry.transform.setTransform
|
||||
function transform:init(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
self:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
end
|
||||
|
||||
--- Constructs a transform.
|
||||
--- @param x number? translation on the x axis
|
||||
--- @param y number? translation on the y axis
|
||||
--- @param rotation number? rotation in radians
|
||||
--- @param scaleX number? scale along the x axis
|
||||
--- @param scaleY number? scale along the y axis
|
||||
--- @param offsetX number? offset along the x axis
|
||||
--- @param offsetY number? offsete along the y axis
|
||||
function transform:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY)
|
||||
self.x = x or self.x or 0
|
||||
self.y = y or self.y or 0
|
||||
self.rotation = rotation or self.rotation or 0
|
||||
self.rotationCos = math.cos(self.rotation)
|
||||
self.rotationSin = math.sin(self.rotation)
|
||||
self.scaleX = scaleX or self.scaleX or 1
|
||||
self.scaleY = scaleY or self.scaleY or 1
|
||||
self.offsetX = offsetX or self.offsetX or 0
|
||||
self.offsetY = offsetY or self.offsetY or 0
|
||||
end
|
||||
|
||||
--- Transforms (x, y) by the transform and returns the transformed coordinates.
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @return number x
|
||||
--- @return number y
|
||||
function transform:transformPoint(x, y)
|
||||
local ox = x - self.offsetX
|
||||
local oy = y - self.offsetY
|
||||
local rx = ox * self.rotationCos - oy * self.rotationSin
|
||||
local ry = ox * self.rotationSin + oy * self.rotationCos
|
||||
local sx = rx * self.scaleX
|
||||
local sy = ry * self.scaleY
|
||||
local resultX = sx + self.x
|
||||
local resultY = sy + self.y
|
||||
|
||||
return resultX, resultY
|
||||
end
|
||||
|
||||
--- Transforms the normal (x, y) by this transform.
|
||||
--- This is essentially the inverse-transpose of just the rotation and scale components.
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @return number x
|
||||
--- @return number y
|
||||
function transform:transformNormal(x, y)
|
||||
local sx = x / self.scaleX
|
||||
local sy = y / self.scaleY
|
||||
local resultX = sx * self.rotationCos - sy * self.rotationSin
|
||||
local resultY = sx * self.rotationSin + sy * self.rotationCos
|
||||
|
||||
return resultX, resultY
|
||||
end
|
||||
|
||||
--- Transforms (x, y) by the inverse of the transform and returns the inverse transformed coordinates.
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @return number x
|
||||
--- @return number y
|
||||
function transform:inverseTransformPoint(x, y)
|
||||
local tx = x - self.x
|
||||
local ty = y - self.y
|
||||
local sx = tx / self.scaleX
|
||||
local sy = ty / self.scaleY
|
||||
local rx = sx * self.rotationCos + sy * self.rotationSin
|
||||
local ry = sy * self.rotationCos - sx * self.rotationSin
|
||||
local resultX = rx + self.offsetX
|
||||
local resultY = ry + self.offsetY
|
||||
|
||||
return resultX, resultY
|
||||
end
|
||||
|
||||
--- Copies this transform to `other`.
|
||||
--- @param other slick.geometry.transform
|
||||
function transform:copy(other)
|
||||
assert(not other.immutable)
|
||||
|
||||
other.x = self.x
|
||||
other.y = self.y
|
||||
other.rotation = self.rotation
|
||||
other.rotationCos = self.rotationCos
|
||||
other.rotationSin = self.rotationSin
|
||||
other.scaleX = self.scaleX
|
||||
other.scaleY = self.scaleY
|
||||
other.offsetX = self.offsetX
|
||||
other.offsetY = self.offsetY
|
||||
end
|
||||
|
||||
transform.IDENTITY = transform._newImmutable()
|
||||
|
||||
return transform
|
||||
1879
game/love_src/lib/slick/geometry/triangulation/delaunay.lua
Normal file
1879
game/love_src/lib/slick/geometry/triangulation/delaunay.lua
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
||||
local segment = require("slick.geometry.segment")
|
||||
local edge = require("slick.geometry.triangulation.edge")
|
||||
|
||||
--- @class slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @field edge slick.geometry.triangulation.edge
|
||||
--- @field segment slick.geometry.segment
|
||||
local delaunaySortedEdge = {}
|
||||
local metatable = { __index = delaunaySortedEdge }
|
||||
|
||||
--- @return slick.geometry.triangulation.delaunaySortedEdge
|
||||
function delaunaySortedEdge.new()
|
||||
return setmetatable({
|
||||
edge = edge.new(),
|
||||
segment = segment.new()
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @return slick.util.search.compareResult
|
||||
function delaunaySortedEdge.compare(a, b)
|
||||
return segment.compare(a.segment, b.segment, 0)
|
||||
end
|
||||
|
||||
--- @param sortedEdge slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @param segment slick.geometry.segment
|
||||
--- @return slick.util.search.compareResult
|
||||
function delaunaySortedEdge.compareSegment(sortedEdge, segment)
|
||||
return segment.compare(sortedEdge.segment, segment, 0)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedEdge
|
||||
--- @return boolean
|
||||
function delaunaySortedEdge.less(a, b)
|
||||
return delaunaySortedEdge.compare(a, b) < 0
|
||||
end
|
||||
|
||||
--- @param e slick.geometry.triangulation.edge
|
||||
--- @param segment slick.geometry.segment
|
||||
function delaunaySortedEdge:init(e, segment)
|
||||
self.edge:init(e.a, e.b)
|
||||
self.segment:init(segment.a, segment.b)
|
||||
end
|
||||
|
||||
return delaunaySortedEdge
|
||||
@ -0,0 +1,62 @@
|
||||
local point = require("slick.geometry.point")
|
||||
|
||||
--- @class slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @field point slick.geometry.point
|
||||
--- @field id number
|
||||
--- @field newID number
|
||||
local delaunaySortedPoint = {}
|
||||
local metatable = { __index = delaunaySortedPoint }
|
||||
|
||||
--- @return slick.geometry.triangulation.delaunaySortedPoint
|
||||
function delaunaySortedPoint.new()
|
||||
return setmetatable({
|
||||
id = 0,
|
||||
newID = 0,
|
||||
point = point.new()
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param s slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @param p slick.geometry.point
|
||||
--- @return slick.util.search.compareResult
|
||||
function delaunaySortedPoint.comparePoint(s, p)
|
||||
return point.compare(s.point, p)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @return slick.util.search.compareResult
|
||||
function delaunaySortedPoint.compare(a, b)
|
||||
return point.compare(a.point, b.point)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @return slick.util.search.compareResult
|
||||
function delaunaySortedPoint.compareID(a, b)
|
||||
return a.id - b.id
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @return boolean
|
||||
function delaunaySortedPoint.less(a, b)
|
||||
return delaunaySortedPoint.compare(a, b) < 0
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @param b slick.geometry.triangulation.delaunaySortedPoint
|
||||
--- @return boolean
|
||||
function delaunaySortedPoint.lessID(a, b)
|
||||
return delaunaySortedPoint.compareID(a, b) < 0
|
||||
end
|
||||
|
||||
--- @param point slick.geometry.point
|
||||
--- @param id number
|
||||
function delaunaySortedPoint:init(point, id)
|
||||
self.id = id
|
||||
self.newID = 0
|
||||
self.point:init(point.x, point.y)
|
||||
end
|
||||
|
||||
return delaunaySortedPoint
|
||||
37
game/love_src/lib/slick/geometry/triangulation/dissolve.lua
Normal file
37
game/love_src/lib/slick/geometry/triangulation/dissolve.lua
Normal file
@ -0,0 +1,37 @@
|
||||
local point = require("slick.geometry.point")
|
||||
|
||||
--- @class slick.geometry.triangulation.dissolve
|
||||
--- @field point slick.geometry.point
|
||||
--- @field index number
|
||||
--- @field userdata any?
|
||||
--- @field otherIndex number
|
||||
--- @field otherUserdata any?
|
||||
local dissolve = {}
|
||||
local metatable = { __index = dissolve }
|
||||
|
||||
function dissolve.new()
|
||||
return setmetatable({
|
||||
point = point.new()
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @param index number
|
||||
--- @param userdata any?
|
||||
--- @param otherIndex number
|
||||
--- @param otherUserdata any
|
||||
function dissolve:init(p, index, userdata, otherIndex, otherUserdata)
|
||||
self.point:init(p.x, p.y)
|
||||
self.index = index
|
||||
self.userdata = userdata
|
||||
self.otherIndex = otherIndex
|
||||
self.otherUserdata = otherUserdata
|
||||
self.resultUserdata = nil
|
||||
end
|
||||
|
||||
--- @param d slick.geometry.triangulation.dissolve
|
||||
function dissolve.default(d)
|
||||
-- No-op.
|
||||
end
|
||||
|
||||
return dissolve
|
||||
52
game/love_src/lib/slick/geometry/triangulation/edge.lua
Normal file
52
game/love_src/lib/slick/geometry/triangulation/edge.lua
Normal file
@ -0,0 +1,52 @@
|
||||
--- @class slick.geometry.triangulation.edge
|
||||
--- @field a number
|
||||
--- @field b number
|
||||
--- @field min number
|
||||
--- @field max number
|
||||
local edge = {}
|
||||
local metatable = { __index = edge }
|
||||
|
||||
--- @param a number?
|
||||
--- @param b number?
|
||||
--- @return slick.geometry.triangulation.edge
|
||||
function edge.new(a, b)
|
||||
return setmetatable({
|
||||
a = a,
|
||||
b = b,
|
||||
min = a and b and math.min(a, b),
|
||||
max = a and b and math.max(a, b),
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.edge
|
||||
--- @param b slick.geometry.triangulation.edge
|
||||
function edge.less(a, b)
|
||||
if a.min == b.min then
|
||||
return a.max < b.max
|
||||
end
|
||||
|
||||
return a.min < b.min
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.edge
|
||||
--- @param b slick.geometry.triangulation.edge
|
||||
--- @return -1 | 0 | 1
|
||||
function edge.compare(a, b)
|
||||
local min = a.min - b.min
|
||||
if min ~= 0 then
|
||||
return min
|
||||
end
|
||||
|
||||
return a.max - b.max
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
function edge:init(a, b)
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.min = math.min(a, b)
|
||||
self.max = math.max(a, b)
|
||||
end
|
||||
|
||||
return edge
|
||||
69
game/love_src/lib/slick/geometry/triangulation/hull.lua
Normal file
69
game/love_src/lib/slick/geometry/triangulation/hull.lua
Normal file
@ -0,0 +1,69 @@
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.geometry.triangulation.hull
|
||||
--- @field a slick.geometry.point
|
||||
--- @field b slick.geometry.point
|
||||
--- @field lowerPoints number[]
|
||||
--- @field higherPoints number[]
|
||||
--- @field index number
|
||||
local hull = {}
|
||||
local metatable = { __index = hull }
|
||||
|
||||
--- @return slick.geometry.triangulation.hull
|
||||
function hull.new()
|
||||
return setmetatable({
|
||||
higherPoints = {},
|
||||
lowerPoints = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param hull slick.geometry.triangulation.hull
|
||||
--- @param point slick.geometry.point
|
||||
--- @return slick.util.search.compareResult
|
||||
function hull.point(hull, point)
|
||||
return slickmath.direction(hull.a, hull.b, point)
|
||||
end
|
||||
|
||||
--- @param hull slick.geometry.triangulation.hull
|
||||
--- @param sweep slick.geometry.triangulation.sweep
|
||||
--- @return slick.util.search.compareResult
|
||||
function hull.sweep(hull, sweep)
|
||||
local direction
|
||||
|
||||
if hull.a.x < sweep.data.a.x then
|
||||
direction = slickmath.direction(hull.a, hull.b, sweep.data.a)
|
||||
else
|
||||
direction = slickmath.direction(sweep.data.b, sweep.data.a, hull.a)
|
||||
end
|
||||
|
||||
if direction ~= 0 then
|
||||
return direction
|
||||
end
|
||||
|
||||
if sweep.data.b.x < hull.b.x then
|
||||
direction = slickmath.direction(hull.a, hull.b, sweep.data.b)
|
||||
else
|
||||
direction = slickmath.direction(sweep.data.b, sweep.data.a, hull.b)
|
||||
end
|
||||
|
||||
if direction ~= 0 then
|
||||
return direction
|
||||
end
|
||||
|
||||
return hull.index - sweep.index
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param index number
|
||||
function hull:init(a, b, index)
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.index = index
|
||||
|
||||
slicktable.clear(self.higherPoints)
|
||||
slicktable.clear(self.lowerPoints)
|
||||
end
|
||||
|
||||
return hull
|
||||
10
game/love_src/lib/slick/geometry/triangulation/init.lua
Normal file
10
game/love_src/lib/slick/geometry/triangulation/init.lua
Normal file
@ -0,0 +1,10 @@
|
||||
return {
|
||||
delaunay = require("slick.geometry.triangulation.delaunay"),
|
||||
delaunaySortedEdge = require("slick.geometry.triangulation.delaunaySortedEdge"),
|
||||
delaunaySortedPoint = require("slick.geometry.triangulation.delaunaySortedPoint"),
|
||||
dissolve = require("slick.geometry.triangulation.dissolve"),
|
||||
edge = require("slick.geometry.triangulation.edge"),
|
||||
hull = require("slick.geometry.triangulation.hull"),
|
||||
intersection = require("slick.geometry.triangulation.intersection"),
|
||||
sweep = require("slick.geometry.triangulation.sweep"),
|
||||
}
|
||||
109
game/love_src/lib/slick/geometry/triangulation/intersection.lua
Normal file
109
game/love_src/lib/slick/geometry/triangulation/intersection.lua
Normal file
@ -0,0 +1,109 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
|
||||
--- @class slick.geometry.triangulation.intersection
|
||||
--- @field a1 slick.geometry.point
|
||||
--- @field a1Index number
|
||||
--- @field a1Userdata any?
|
||||
--- @field b1 slick.geometry.point
|
||||
--- @field b1Index number
|
||||
--- @field b1Userdata any?
|
||||
--- @field delta1 number
|
||||
--- @field a2 slick.geometry.point
|
||||
--- @field a2Index number
|
||||
--- @field a2Userdata any?
|
||||
--- @field b2 slick.geometry.point
|
||||
--- @field b2Index number
|
||||
--- @field b2Userdata any?
|
||||
--- @field delta2 number
|
||||
--- @field result slick.geometry.point
|
||||
--- @field resultIndex number
|
||||
--- @field resultUserdata any?
|
||||
--- @field private s slick.geometry.point
|
||||
--- @field private t slick.geometry.point
|
||||
--- @field private p slick.geometry.point
|
||||
--- @field private q slick.geometry.point
|
||||
local intersection = {}
|
||||
local metatable = { __index = intersection }
|
||||
|
||||
function intersection.new()
|
||||
return setmetatable({
|
||||
a1 = point.new(),
|
||||
b1 = point.new(),
|
||||
a2 = point.new(),
|
||||
b2 = point.new(),
|
||||
a1Index = 0,
|
||||
b1Index = 0,
|
||||
a2Index = 0,
|
||||
b2Index = 0,
|
||||
result = point.new(),
|
||||
delta1 = 0,
|
||||
delta2 = 0,
|
||||
s = point.new(),
|
||||
t = point.new(),
|
||||
p = point.new(),
|
||||
q = point.new(),
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param aIndex number
|
||||
--- @param bIndex number
|
||||
--- @param delta number
|
||||
--- @param aUserdata any
|
||||
--- @param bUserdata any
|
||||
function intersection:setLeftEdge(a, b, aIndex, bIndex, delta, aUserdata, bUserdata)
|
||||
self.a1:init(a.x, a.y)
|
||||
self.b1:init(b.x, b.y)
|
||||
self.a1Index = aIndex
|
||||
self.b1Index = bIndex
|
||||
self.a1Userdata = aUserdata
|
||||
self.b1Userdata = bUserdata
|
||||
self.delta1 = delta
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param aIndex number
|
||||
--- @param bIndex number
|
||||
--- @param delta number
|
||||
--- @param aUserdata any
|
||||
--- @param bUserdata any
|
||||
function intersection:setRightEdge(a, b, aIndex, bIndex, delta, aUserdata, bUserdata)
|
||||
self.a2:init(a.x, a.y)
|
||||
self.b2:init(b.x, b.y)
|
||||
self.a2Index = aIndex
|
||||
self.b2Index = bIndex
|
||||
self.a2Userdata = aUserdata
|
||||
self.b2Userdata = bUserdata
|
||||
self.delta2 = delta
|
||||
end
|
||||
|
||||
--- @param resultIndex number
|
||||
function intersection:init(resultIndex)
|
||||
self.a1Userdata = nil
|
||||
self.b1Userdata = nil
|
||||
self.a2Userdata = nil
|
||||
self.b2Userdata = nil
|
||||
self.resultUserdata = nil
|
||||
self.resultIndex = resultIndex
|
||||
end
|
||||
|
||||
--- @param i slick.geometry.triangulation.intersection
|
||||
function intersection.default(i)
|
||||
-- No-op.
|
||||
end
|
||||
|
||||
--- @param userdata any?
|
||||
--- @param x number?
|
||||
--- @param y number?
|
||||
function intersection:setResult(userdata, x, y)
|
||||
self.resultUserdata = userdata
|
||||
|
||||
if x and y then
|
||||
self.result:init(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
return intersection
|
||||
32
game/love_src/lib/slick/geometry/triangulation/map.lua
Normal file
32
game/love_src/lib/slick/geometry/triangulation/map.lua
Normal file
@ -0,0 +1,32 @@
|
||||
local point = require("slick.geometry.point")
|
||||
|
||||
--- @class slick.geometry.triangulation.map
|
||||
--- @field point slick.geometry.point
|
||||
--- @field index number
|
||||
--- @field userdata any?
|
||||
--- @field otherIndex number
|
||||
--- @field otherUserdata any?
|
||||
local map = {}
|
||||
local metatable = { __index = map }
|
||||
|
||||
function map.new()
|
||||
return setmetatable({
|
||||
point = point.new()
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param p slick.geometry.point
|
||||
--- @param oldIndex number
|
||||
--- @param newIndex number
|
||||
function map:init(p, oldIndex, newIndex)
|
||||
self.point:init(p.x, p.y)
|
||||
self.oldIndex = oldIndex
|
||||
self.newIndex = newIndex
|
||||
end
|
||||
|
||||
--- @param d slick.geometry.triangulation.map
|
||||
function map.default(d)
|
||||
-- No-op.
|
||||
end
|
||||
|
||||
return map
|
||||
78
game/love_src/lib/slick/geometry/triangulation/sweep.lua
Normal file
78
game/love_src/lib/slick/geometry/triangulation/sweep.lua
Normal file
@ -0,0 +1,78 @@
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
local point = require("slick.geometry.point")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local util = require("slick.util")
|
||||
|
||||
--- @class slick.geometry.triangulation.sweep
|
||||
--- @field type slick.geometry.triangulation.sweepType
|
||||
--- @field data slick.geometry.point | slick.geometry.segment
|
||||
--- @field point slick.geometry.point?
|
||||
--- @field index number
|
||||
local sweep = {}
|
||||
local metatable = { __index = sweep }
|
||||
|
||||
--- @alias slick.geometry.triangulation.sweepType 0 | 1 | 2 | 3
|
||||
sweep.TYPE_NONE = 0
|
||||
sweep.TYPE_POINT = 1
|
||||
sweep.TYPE_EDGE_STOP = 2
|
||||
sweep.TYPE_EDGE_START = 3
|
||||
|
||||
--- @return slick.geometry.triangulation.sweep
|
||||
function sweep.new()
|
||||
return setmetatable({
|
||||
type = sweep.TYPE_NONE,
|
||||
index = 0
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param data slick.geometry.point | slick.geometry.segment
|
||||
--- @return slick.geometry.point
|
||||
local function _getPointFromData(data)
|
||||
if util.is(data, segment) then
|
||||
return data.a
|
||||
elseif util.is(data, point) then
|
||||
--- @cast data slick.geometry.point
|
||||
return data
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line: missing-return
|
||||
assert(false, "expected 'slick.geometry.point' or 'slick.geometry.segment'")
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.triangulation.sweep
|
||||
--- @param b slick.geometry.triangulation.sweep
|
||||
--- @return boolean
|
||||
function sweep.less(a, b)
|
||||
if a.point:lessThan(b.point) then
|
||||
return true
|
||||
elseif a.point:equal(b.point) then
|
||||
if a.type < b.type then
|
||||
return true
|
||||
elseif a.type == b.type then
|
||||
if a.type == sweep.TYPE_EDGE_START or a.type == sweep.TYPE_EDGE_STOP then
|
||||
local direction = slickmath.direction(a.point, b.point, b.data.b)
|
||||
if direction ~= 0 then
|
||||
return direction < 0
|
||||
end
|
||||
end
|
||||
|
||||
return a.index < b.index
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- @param sweepType slick.geometry.triangulation.sweepType
|
||||
--- @param data slick.geometry.point | slick.geometry.segment
|
||||
--- @param index number
|
||||
function sweep:init(sweepType, data, index)
|
||||
self.type = sweepType
|
||||
self.data = data
|
||||
self.index = index
|
||||
self.point = _getPointFromData(data)
|
||||
end
|
||||
|
||||
return sweep
|
||||
142
game/love_src/lib/slick/init.lua
Normal file
142
game/love_src/lib/slick/init.lua
Normal file
@ -0,0 +1,142 @@
|
||||
local PATH = (...):gsub("[^%.]+$", "")
|
||||
|
||||
--- @module "slick.cache"
|
||||
local cache
|
||||
|
||||
--- @module "slick.collision"
|
||||
local collision
|
||||
|
||||
--- @module "slick.draw"
|
||||
local draw
|
||||
|
||||
--- @module "slick.entity"
|
||||
local entity
|
||||
|
||||
--- @module "slick.enum"
|
||||
local enum
|
||||
|
||||
--- @module "slick.geometry"
|
||||
local geometry
|
||||
|
||||
--- @module "slick.navigation"
|
||||
local navigation
|
||||
|
||||
--- @module "slick.options"
|
||||
local defaultOptions
|
||||
|
||||
--- @module "slick.responses"
|
||||
local responses
|
||||
|
||||
--- @module "slick.shape"
|
||||
local shape
|
||||
|
||||
--- @module "slick.tag"
|
||||
local tag
|
||||
|
||||
--- @module "slick.util"
|
||||
local util
|
||||
|
||||
--- @module "slick.world"
|
||||
local world
|
||||
|
||||
--- @module "slick.worldQuery"
|
||||
local worldQuery
|
||||
|
||||
--- @module "slick.worldQueryResponse"
|
||||
local worldQueryResponse
|
||||
|
||||
--- @module "slick.meta"
|
||||
local meta
|
||||
|
||||
local function load()
|
||||
local requireImpl = require
|
||||
local require = function(path)
|
||||
return requireImpl(PATH .. path)
|
||||
end
|
||||
|
||||
local patchedG = {
|
||||
__index = _G
|
||||
}
|
||||
|
||||
local g = { require = require }
|
||||
g._G = g
|
||||
|
||||
setfenv(0, setmetatable(g, patchedG))
|
||||
|
||||
cache = require("slick.cache")
|
||||
collision = require("slick.collision")
|
||||
draw = require("slick.draw")
|
||||
entity = require("slick.entity")
|
||||
enum = require("slick.enum")
|
||||
geometry = require("slick.geometry")
|
||||
navigation = require("slick.navigation")
|
||||
defaultOptions = require("slick.options")
|
||||
responses = require("slick.responses")
|
||||
shape = require("slick.shape")
|
||||
tag = require("slick.tag")
|
||||
util = require("slick.util")
|
||||
world = require("slick.world")
|
||||
worldQuery = require("slick.worldQuery")
|
||||
worldQueryResponse = require("slick.worldQueryResponse")
|
||||
|
||||
meta = require("slick.meta")
|
||||
end
|
||||
|
||||
do
|
||||
local l = coroutine.create(load)
|
||||
repeat
|
||||
local s, r = coroutine.resume(l)
|
||||
if not s then
|
||||
error(debug.traceback(l, r))
|
||||
end
|
||||
until coroutine.status(l) == "dead"
|
||||
end
|
||||
|
||||
return {
|
||||
_VERSION = meta._VERSION,
|
||||
_DESCRIPTION = meta._DESCRIPTION,
|
||||
_URL = meta._URL,
|
||||
_LICENSE = meta._LICENSE,
|
||||
|
||||
cache = cache,
|
||||
collision = collision,
|
||||
defaultOptions = defaultOptions,
|
||||
entity = entity,
|
||||
geometry = geometry,
|
||||
shape = shape,
|
||||
tag = tag,
|
||||
util = util,
|
||||
world = world,
|
||||
worldQuery = worldQuery,
|
||||
worldQueryResponse = worldQueryResponse,
|
||||
responses = responses,
|
||||
|
||||
newCache = cache.new,
|
||||
newWorld = world.new,
|
||||
newWorldQuery = worldQuery.new,
|
||||
newTransform = geometry.transform.new,
|
||||
|
||||
newRectangleShape = shape.newRectangle,
|
||||
newChainShape = shape.newChain,
|
||||
newCircleShape = shape.newCircle,
|
||||
newLineSegmentShape = shape.newLineSegment,
|
||||
newPolygonShape = shape.newPolygon,
|
||||
newPolygonMeshShape = shape.newPolygonMesh,
|
||||
newPolylineShape = shape.newPolyline,
|
||||
newMeshShape = shape.newMesh,
|
||||
newShapeGroup = shape.newShapeGroup,
|
||||
newEnum = enum.new,
|
||||
newTag = tag.new,
|
||||
|
||||
triangulate = geometry.simple.triangulate,
|
||||
polygonize = geometry.simple.polygonize,
|
||||
clip = geometry.simple.clip,
|
||||
|
||||
newUnionClipOperation = geometry.simple.newUnionClipOperation,
|
||||
newIntersectionClipOperation = geometry.simple.newIntersectionClipOperation,
|
||||
newDifferenceClipOperation = geometry.simple.newDifferenceClipOperation,
|
||||
|
||||
navigation = navigation,
|
||||
|
||||
drawWorld = draw
|
||||
}
|
||||
380
game/love_src/lib/slick/meta/init.lua
Normal file
380
game/love_src/lib/slick/meta/init.lua
Normal file
@ -0,0 +1,380 @@
|
||||
return {
|
||||
_VERSION = "4.0.8",
|
||||
_DESCRIPTION = "slick is a simple to use polygon collision library inspired by bump.lua",
|
||||
_URL = "https://github.com/erinmaus/slick",
|
||||
_LICENSE = [[
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
]]
|
||||
}
|
||||
49
game/love_src/lib/slick/navigation/edge.lua
Normal file
49
game/love_src/lib/slick/navigation/edge.lua
Normal file
@ -0,0 +1,49 @@
|
||||
--- @class slick.navigation.edge
|
||||
--- @field a slick.navigation.vertex
|
||||
--- @field b slick.navigation.vertex
|
||||
--- @field min number
|
||||
--- @field max number
|
||||
local edge = {}
|
||||
local metatable = { __index = edge }
|
||||
|
||||
--- @param a slick.navigation.vertex
|
||||
--- @param b slick.navigation.vertex
|
||||
--- @param interior boolean?
|
||||
--- @param exterior boolean?
|
||||
--- @return slick.navigation.edge
|
||||
function edge.new(a, b, interior, exterior)
|
||||
return setmetatable({
|
||||
a = a,
|
||||
b = b,
|
||||
min = math.min(a.index, b.index),
|
||||
max = math.max(a.index, b.index),
|
||||
interior = not not interior,
|
||||
exterior = not not exterior
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param other slick.navigation.edge
|
||||
--- @return boolean
|
||||
function edge:same(other)
|
||||
return self == other or (self.min == other.min and self.max == other.max)
|
||||
end
|
||||
|
||||
--- @param a slick.navigation.edge
|
||||
--- @param b slick.navigation.edge
|
||||
--- @return -1 | 0 | 1
|
||||
function edge.compare(a, b)
|
||||
if a.min == b.min then
|
||||
return a.max - b.max
|
||||
else
|
||||
return a.min - b.min
|
||||
end
|
||||
end
|
||||
|
||||
--- @param a slick.navigation.edge
|
||||
--- @param b slick.navigation.edge
|
||||
--- @return boolean
|
||||
function edge.less(a, b)
|
||||
return edge.compare(a, b) < 0
|
||||
end
|
||||
|
||||
return edge
|
||||
8
game/love_src/lib/slick/navigation/init.lua
Normal file
8
game/love_src/lib/slick/navigation/init.lua
Normal file
@ -0,0 +1,8 @@
|
||||
return {
|
||||
edge = require("slick.navigation.edge"),
|
||||
mesh = require("slick.navigation.mesh"),
|
||||
meshBuilder = require("slick.navigation.meshBuilder"),
|
||||
path = require("slick.navigation.path"),
|
||||
triangle = require("slick.navigation.triangle"),
|
||||
vertex = require("slick.navigation.vertex"),
|
||||
}
|
||||
432
game/love_src/lib/slick/navigation/mesh.lua
Normal file
432
game/love_src/lib/slick/navigation/mesh.lua
Normal file
@ -0,0 +1,432 @@
|
||||
local quadTree = require "slick.collision.quadTree"
|
||||
local quadTreeQuery = require "slick.collision.quadTreeQuery"
|
||||
local point = require "slick.geometry.point"
|
||||
local rectangle = require "slick.geometry.rectangle"
|
||||
local segment = require "slick.geometry.segment"
|
||||
local edge = require "slick.navigation.edge"
|
||||
local triangle = require "slick.navigation.triangle"
|
||||
local vertex = require "slick.navigation.vertex"
|
||||
local search = require "slick.util.search"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
|
||||
--- @class slick.navigation.mesh
|
||||
--- @field vertices slick.navigation.vertex[]
|
||||
--- @field edges slick.navigation.edge[]
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field vertexNeighbors table<number, slick.navigation.edge[]>
|
||||
--- @field triangles slick.navigation.triangle[]
|
||||
--- @field inputPoints number[]
|
||||
--- @field inputEdges number[]
|
||||
--- @field inputExteriorEdges number[]
|
||||
--- @field inputInteriorEdges number[]
|
||||
--- @field inputUserdata any[]
|
||||
--- @field vertexToTriangle table<number, slick.navigation.triangle[]>
|
||||
--- @field triangleNeighbors table<number, slick.navigation.triangle[]>
|
||||
--- @field sharedTriangleEdges table<number, table<number, slick.navigation.edge>>
|
||||
--- @field edgeTriangles table<slick.navigation.edge, slick.navigation.triangle[]>
|
||||
--- @field quadTree slick.collision.quadTree?
|
||||
--- @field quadTreeQuery slick.collision.quadTreeQuery?
|
||||
local mesh = {}
|
||||
local metatable = { __index = mesh }
|
||||
|
||||
|
||||
--- @param aTriangles slick.navigation.triangle[]
|
||||
--- @param bTriangles slick.navigation.triangle[]
|
||||
--- @param e slick.navigation.edge
|
||||
--- @return slick.navigation.triangle?, slick.navigation.triangle?
|
||||
local function _findSharedTriangle(aTriangles, bTriangles, e)
|
||||
for _, t1 in ipairs(aTriangles) do
|
||||
for _, t2 in ipairs(bTriangles) do
|
||||
if t1.index ~= t2.index then
|
||||
if t1.vertices[e.a.index] and t1.vertices[e.b.index] and t2.vertices[e.a.index] and t2.vertices[e.b.index] then
|
||||
if t1.index ~= t2.index then
|
||||
return t1, t2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--- @overload fun(points: number[], userdata: any[], edges: number[]): slick.navigation.mesh
|
||||
--- @overload fun(points: number[], userdata: any[], exteriorEdges: number[], interiorEdges: number[]): slick.navigation.mesh
|
||||
--- @overload fun(points: number[], userdata: any[], edges: number[], triangles: number[][]): slick.navigation.mesh
|
||||
--- @return slick.navigation.mesh
|
||||
function mesh.new(points, userdata, edges, z)
|
||||
local self = setmetatable({
|
||||
vertices = {},
|
||||
edges = {},
|
||||
vertexNeighbors = {},
|
||||
triangleNeighbors = {},
|
||||
triangles = {},
|
||||
inputPoints = {},
|
||||
inputEdges = {},
|
||||
inputUserdata = {},
|
||||
inputExteriorEdges = {},
|
||||
inputInteriorEdges = {},
|
||||
vertexToTriangle = {},
|
||||
sharedTriangleEdges = {},
|
||||
edgeTriangles = {},
|
||||
bounds = rectangle.new(points[1], points[2], points[1], points[2])
|
||||
}, metatable)
|
||||
|
||||
for i = 1, #points, 2 do
|
||||
local n = (i - 1) / 2 + 1
|
||||
local vertex = vertex.new(point.new(points[i], points[i + 1]), userdata and userdata[n] or nil, n)
|
||||
|
||||
table.insert(self.vertices, vertex)
|
||||
|
||||
table.insert(self.inputPoints, points[i])
|
||||
table.insert(self.inputPoints, points[i + 1])
|
||||
|
||||
self.inputUserdata[n] = userdata and userdata[n] or nil
|
||||
|
||||
self.bounds:expand(points[i], points[i + 1])
|
||||
end
|
||||
|
||||
for i = 1, #edges, 2 do
|
||||
table.insert(self.inputEdges, edges[i])
|
||||
table.insert(self.inputEdges, edges[i + 1])
|
||||
table.insert(self.inputExteriorEdges, edges[i])
|
||||
table.insert(self.inputExteriorEdges, edges[i + 1])
|
||||
end
|
||||
|
||||
if z and type(z) == "table" and #z >= 1 and type(z[1]) == "table" and #z[1] == 3 then
|
||||
for _, t in ipairs(z) do
|
||||
local n = triangle.new(self.vertices[t[1]], self.vertices[t[2]], self.vertices[t[3]], #self.triangles + 1)
|
||||
|
||||
for i = 1, #t do
|
||||
local j = (i % #t) + 1
|
||||
|
||||
local s = t[i]
|
||||
local t = t[j]
|
||||
|
||||
local e1 = edge.new(self.vertices[s], self.vertices[t])
|
||||
local e2 = edge.new(self.vertices[t], self.vertices[s])
|
||||
table.insert(self.edges, e1)
|
||||
|
||||
local neighborsI = self.vertexNeighbors[s]
|
||||
if not neighborsI then
|
||||
neighborsI = {}
|
||||
self.vertexNeighbors[s] = neighborsI
|
||||
end
|
||||
|
||||
local neighborsJ = self.vertexNeighbors[t]
|
||||
if not neighborsJ then
|
||||
neighborsJ = {}
|
||||
self.vertexNeighbors[t] = neighborsJ
|
||||
end
|
||||
|
||||
do
|
||||
local hasE = false
|
||||
for _, neighbor in ipairs(neighborsI) do
|
||||
if neighbor.min == e1.min and neighbor.max == e1.max then
|
||||
hasE = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hasE then
|
||||
table.insert(neighborsI, e1)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local hasE = false
|
||||
for _, neighbor in ipairs(neighborsJ) do
|
||||
if neighbor.min == e2.min and neighbor.max == e2.max then
|
||||
hasE = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hasE then
|
||||
table.insert(neighborsJ, e2)
|
||||
end
|
||||
end
|
||||
|
||||
local v = self.vertexToTriangle[s]
|
||||
if not v then
|
||||
v = {}
|
||||
self.vertexToTriangle[s] = v
|
||||
end
|
||||
|
||||
table.insert(v, n)
|
||||
end
|
||||
|
||||
table.insert(self.triangles, n)
|
||||
end
|
||||
|
||||
table.sort(self.edges, edge.less)
|
||||
|
||||
for _, e in ipairs(self.edges) do
|
||||
local aTriangles = self.vertexToTriangle[e.a.index]
|
||||
local bTriangles = self.vertexToTriangle[e.b.index]
|
||||
|
||||
local a, b = _findSharedTriangle(aTriangles, bTriangles, e)
|
||||
if a and b then
|
||||
self.edgeTriangles[e] = { a, b }
|
||||
|
||||
do
|
||||
local x = self.sharedTriangleEdges[a.index]
|
||||
if not x then
|
||||
x = {}
|
||||
self.sharedTriangleEdges[a.index] = x
|
||||
end
|
||||
|
||||
x[b.index] = e
|
||||
end
|
||||
|
||||
do
|
||||
local x = self.sharedTriangleEdges[b.index]
|
||||
if not x then
|
||||
x = {}
|
||||
self.sharedTriangleEdges[b.index] = x
|
||||
end
|
||||
|
||||
x[a.index] = e
|
||||
end
|
||||
|
||||
do
|
||||
local neighbors = self.triangleNeighbors[a.index]
|
||||
if neighbors == nil then
|
||||
neighbors = {}
|
||||
self.triangleNeighbors[a.index] = neighbors
|
||||
end
|
||||
|
||||
local hasT = false
|
||||
for _, neighbor in ipairs(neighbors) do
|
||||
if neighbor.index == b.index then
|
||||
hasT = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hasT then
|
||||
table.insert(neighbors, b)
|
||||
assert(#neighbors <= 3)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local neighbors = self.triangleNeighbors[b.index]
|
||||
if neighbors == nil then
|
||||
neighbors = {}
|
||||
self.triangleNeighbors[b.index] = neighbors
|
||||
end
|
||||
|
||||
local hasT = false
|
||||
for _, neighbor in ipairs(neighbors) do
|
||||
if neighbor.index == a.index then
|
||||
hasT = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not hasT then
|
||||
table.insert(neighbors, a)
|
||||
assert(#neighbors <= 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif z and type(z) == "table" and #z >= 2 and #z % 2 == 0 and type(z[1]) == "number" then
|
||||
for i = 1, #z, 2 do
|
||||
table.insert(self.inputEdges, z[i])
|
||||
table.insert(self.inputEdges, z[i + 1])
|
||||
table.insert(self.inputInteriorEdges, z[i])
|
||||
table.insert(self.inputInteriorEdges, z[i + 1])
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- @type slick.collision.quadTreeOptions
|
||||
local _quadTreeOptions = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = 0,
|
||||
height = 0
|
||||
}
|
||||
|
||||
--- @private
|
||||
function mesh:_buildQuadTree()
|
||||
_quadTreeOptions.x = self.bounds:left()
|
||||
_quadTreeOptions.y = self.bounds:top()
|
||||
_quadTreeOptions.width = self.bounds:width()
|
||||
_quadTreeOptions.height = self.bounds:height()
|
||||
|
||||
self.quadTree = quadTree.new(_quadTreeOptions)
|
||||
self.quadTreeQuery = quadTreeQuery.new(self.quadTree)
|
||||
|
||||
for _, triangle in ipairs(self.triangles) do
|
||||
self.quadTree:insert(triangle, triangle.bounds)
|
||||
end
|
||||
end
|
||||
|
||||
local _getTrianglePoint = point.new()
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @return slick.navigation.triangle | nil
|
||||
function mesh:getContainingTriangle(x, y)
|
||||
if not self.quadTree then
|
||||
self:_buildQuadTree()
|
||||
end
|
||||
|
||||
_getTrianglePoint:init(x, y)
|
||||
self.quadTreeQuery:perform(_getTrianglePoint, slickmath.EPSILON)
|
||||
|
||||
for _, hit in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast hit slick.navigation.triangle
|
||||
|
||||
local inside = true
|
||||
local currentSide
|
||||
for i = 1, #hit.triangle do
|
||||
local side = slickmath.direction(
|
||||
hit.triangle[i].point,
|
||||
hit.triangle[(i % #hit.triangle) + 1].point,
|
||||
_getTrianglePoint)
|
||||
|
||||
-- Point is collinear with edge.
|
||||
-- We consider this inside.
|
||||
if side == 0 then
|
||||
break
|
||||
end
|
||||
|
||||
if not currentSide then
|
||||
currentSide = side
|
||||
elseif currentSide ~= side then
|
||||
inside = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if inside then
|
||||
return hit
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- @param index number
|
||||
--- @return slick.navigation.vertex
|
||||
function mesh:getVertex(index)
|
||||
return self.vertices[index]
|
||||
end
|
||||
|
||||
--- @param index number
|
||||
--- @return slick.navigation.edge[]
|
||||
function mesh:getTriangleNeighbors(index)
|
||||
return self.triangleNeighbors[index]
|
||||
end
|
||||
|
||||
--- @param index number
|
||||
--- @return slick.navigation.edge[]
|
||||
function mesh:getVertexNeighbors(index)
|
||||
return self.vertexNeighbors[index]
|
||||
end
|
||||
|
||||
local _insideSegment = segment.new()
|
||||
local _triangleSegment = segment.new()
|
||||
local _insideTriangleSegment = segment.new()
|
||||
|
||||
--- @param a slick.navigation.vertex
|
||||
--- @param b slick.navigation.vertex
|
||||
--- @return boolean, number?, number?
|
||||
function mesh:cross(a, b)
|
||||
if not self.quadTree then
|
||||
self:_buildQuadTree()
|
||||
end
|
||||
|
||||
_insideSegment:init(a.point, b.point)
|
||||
self.quadTreeQuery:perform(_insideSegment)
|
||||
|
||||
local hasIntersectedTriangle = false
|
||||
local bestDistance = math.huge
|
||||
local bestX, bestY
|
||||
for _, hit in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast hit slick.navigation.triangle
|
||||
|
||||
local intersectedTriangle = false
|
||||
for i, vertex in ipairs(hit.triangle) do
|
||||
local otherVertex = hit.triangle[(i % #hit.triangle) + 1]
|
||||
|
||||
_triangleSegment:init(vertex.point, otherVertex.point)
|
||||
_insideTriangleSegment:init(vertex.point, otherVertex.point)
|
||||
_insideTriangleSegment.a:init(vertex.point.x, vertex.point.y)
|
||||
_insideTriangleSegment.b:init(otherVertex.point.x, otherVertex.point.y)
|
||||
|
||||
local i, x, y, u, v = slickmath.intersection(vertex.point, otherVertex.point, a.point, b.point, slickmath.EPSILON)
|
||||
if i and u and v then
|
||||
intersectedTriangle = true
|
||||
|
||||
if not self:isSharedEdge(vertex.index, otherVertex.index) then
|
||||
local distance = (x - a.point.x) ^ 2 + (x - a.point.y) ^ 2
|
||||
if distance < bestDistance then
|
||||
bestDistance = distance
|
||||
bestX, bestY = x, y
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
hasIntersectedTriangle = hasIntersectedTriangle or intersectedTriangle
|
||||
end
|
||||
|
||||
return hasIntersectedTriangle, bestX, bestY
|
||||
end
|
||||
|
||||
local _a = vertex.new(point.new(0, 0), nil, 1)
|
||||
local _b = vertex.new(point.new(0, 0), nil, 2)
|
||||
local _edge = edge.new(_a, _b)
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
--- @return slick.navigation.edge
|
||||
function mesh:getEdge(a, b)
|
||||
_edge.a = self.vertices[a]
|
||||
_edge.b = self.vertices[b]
|
||||
_edge.min = math.min(a, b)
|
||||
_edge.max = math.max(a, b)
|
||||
|
||||
local index = search.first(self.edges, _edge, edge.compare)
|
||||
return self.edges[index]
|
||||
end
|
||||
|
||||
--- @param a slick.navigation.triangle
|
||||
--- @param b slick.navigation.triangle
|
||||
--- @return slick.navigation.edge
|
||||
function mesh:getSharedTriangleEdge(a, b)
|
||||
local t = self.sharedTriangleEdges[a.index]
|
||||
local e = t and t[b.index]
|
||||
|
||||
return e
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
function mesh:isSharedEdge(a, b)
|
||||
local edge = self:getEdge(a, b)
|
||||
local triangles = self.edgeTriangles[edge]
|
||||
return triangles ~= nil and #triangles == 2
|
||||
end
|
||||
|
||||
--- @param a number
|
||||
--- @param b number
|
||||
--- @return slick.navigation.triangle ...
|
||||
function mesh:getEdgeTriangles(a, b)
|
||||
local edge = self:getEdge(a, b)
|
||||
local triangles = self.edgeTriangles[edge]
|
||||
if not triangles then
|
||||
return
|
||||
end
|
||||
|
||||
return unpack(triangles)
|
||||
end
|
||||
|
||||
return mesh
|
||||
272
game/love_src/lib/slick/navigation/meshBuilder.lua
Normal file
272
game/love_src/lib/slick/navigation/meshBuilder.lua
Normal file
@ -0,0 +1,272 @@
|
||||
local cache = require "slick.cache"
|
||||
local polygon = require "slick.collision.polygon"
|
||||
local shapeGroup = require "slick.collision.shapeGroup"
|
||||
local clipper = require "slick.geometry.clipper"
|
||||
local enum = require "slick.enum"
|
||||
local mesh = require "slick.navigation.mesh"
|
||||
local tag = require "slick.tag"
|
||||
local util = require "slick.util"
|
||||
local slicktable = require "slick.util.slicktable"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
local lineSegment = require "slick.collision.lineSegment"
|
||||
|
||||
--- @alias slick.navigation.navMeshBuilder.combineMode "union" | "difference"
|
||||
|
||||
--- @alias slick.navigation.navMeshBuilder.layerSettings {
|
||||
--- key: any,
|
||||
--- combineMode: slick.navigation.navMeshBuilder.combineMode,
|
||||
--- mesh: slick.navigation.mesh?,
|
||||
--- }
|
||||
|
||||
--- @class slick.navigation.navMeshBuilder
|
||||
--- @field cache slick.cache
|
||||
--- @field clipper slick.geometry.clipper
|
||||
--- @field layers table<any, slick.navigation.navMeshBuilder.layerSettings>
|
||||
--- @field layerMeshes table<any, slick.navigation.mesh[]>
|
||||
--- @field layerCombineMode slick.navigation.navMeshBuilder.combineMode
|
||||
--- @field private cachedPolygon slick.collision.polygon
|
||||
--- @field private pointsCache number[]
|
||||
--- @field private edgesCache number[]
|
||||
--- @field private userdataCache number[]
|
||||
--- @field private inputTriangles number[][]
|
||||
--- @field private trianglesCache number[][]
|
||||
local navMeshBuilder = {}
|
||||
local metatable = { __index = navMeshBuilder }
|
||||
|
||||
--- @type slick.geometry.triangulation.delaunayTriangulationOptions
|
||||
local triangulationOptions = {
|
||||
refine = true,
|
||||
interior = true,
|
||||
exterior = false,
|
||||
polygonization = false
|
||||
}
|
||||
|
||||
local function _getKey(t)
|
||||
if util.is(t, tag) then
|
||||
return t.value
|
||||
elseif util.is(t, enum) then
|
||||
return t
|
||||
else
|
||||
error("expected tag to be instance of slick.tag or slick.enum")
|
||||
end
|
||||
end
|
||||
|
||||
--- @type slick.options
|
||||
local defaultOptions = {
|
||||
epsilon = slickmath.EPSILON,
|
||||
debug = false
|
||||
}
|
||||
|
||||
--- @param options slick.options?
|
||||
--- @return slick.navigation.navMeshBuilder
|
||||
function navMeshBuilder.new(options)
|
||||
local c = cache.new(options or defaultOptions)
|
||||
return setmetatable({
|
||||
cache = c,
|
||||
clipper = clipper.new(c.triangulator),
|
||||
layers = {},
|
||||
layerMeshes = {},
|
||||
layerSettings = {},
|
||||
cachedPolygon = polygon.new(c),
|
||||
pointsCache = {},
|
||||
edgesCache = {},
|
||||
userdataCache = {},
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param t slick.tag | slick.enum
|
||||
--- @param combineMode slick.navigation.navMeshBuilder.combineMode?
|
||||
function navMeshBuilder:addLayer(t, combineMode)
|
||||
local key = _getKey(t)
|
||||
|
||||
for _, layer in ipairs(self.layers) do
|
||||
if layer.key == key then
|
||||
error("layer already exists")
|
||||
end
|
||||
end
|
||||
|
||||
if combineMode == nil then
|
||||
if #self.layers == 0 then
|
||||
combineMode = "union"
|
||||
else
|
||||
combineMode = "difference"
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(self.layers, {
|
||||
key = key,
|
||||
combineMode = combineMode,
|
||||
points = {},
|
||||
userdata = {},
|
||||
edges = {}
|
||||
})
|
||||
|
||||
self.layerMeshes[key] = {}
|
||||
end
|
||||
|
||||
--- @param t slick.tag | slick.enum
|
||||
--- @param m slick.navigation.mesh
|
||||
function navMeshBuilder:addMesh(t, m)
|
||||
local key = _getKey(t)
|
||||
|
||||
local meshes = self.layerMeshes[key]
|
||||
if not meshes then
|
||||
error("layer with given slick.tag or slick.enum does not exist")
|
||||
end
|
||||
|
||||
table.insert(meshes, m)
|
||||
end
|
||||
|
||||
--- @param shape slick.collision.shape
|
||||
--- @param points number[]
|
||||
--- @param edges number[]
|
||||
--- @param userdata any[]
|
||||
local function _shapeToPointEdges(shape, points, edges, userdata, userdataValue)
|
||||
slicktable.clear(points)
|
||||
slicktable.clear(edges)
|
||||
slicktable.clear(userdata)
|
||||
|
||||
if util.is(shape, lineSegment) then
|
||||
--- @cast shape slick.collision.lineSegment
|
||||
for i = 1, #shape.vertices do
|
||||
local v = shape.vertices[i]
|
||||
|
||||
table.insert(points, v.x)
|
||||
table.insert(points, v.y)
|
||||
|
||||
if i < #shape.vertices then
|
||||
table.insert(edges, i)
|
||||
table.insert(edges, i + 1)
|
||||
end
|
||||
|
||||
if userdataValue ~= nil then
|
||||
table.insert(userdata, userdataValue)
|
||||
end
|
||||
end
|
||||
else
|
||||
|
||||
--- @cast shape slick.collision.commonShape
|
||||
for i, v in ipairs(shape.vertices) do
|
||||
table.insert(points, v.x)
|
||||
table.insert(points, v.y)
|
||||
|
||||
local j = (i % #shape.vertices) + 1
|
||||
table.insert(edges, i)
|
||||
table.insert(edges, j)
|
||||
|
||||
if userdataValue ~= nil then
|
||||
table.insert(userdata, userdataValue)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local _previousShapePoints, _previousShapeEdges, _previousShapeUserdata = {}, {}, {}
|
||||
local _currentShapePoints, _currentShapeEdges, _currentShapeUserdata = {}, {}, {}
|
||||
local _nextShapePoints, _nextShapeEdges, _nextShapeUserdata = {}, {}, {}
|
||||
|
||||
--- @param t slick.tag | slick.enum
|
||||
--- @param shape slick.collision.shapeDefinition
|
||||
--- @param userdata any
|
||||
function navMeshBuilder:addShape(t, shape, userdata)
|
||||
local shapes = shapeGroup.new(self.cache, nil, shape)
|
||||
shapes:attach()
|
||||
|
||||
_shapeToPointEdges(shapes.shapes[1], _previousShapePoints, _previousShapeEdges, _previousShapeUserdata, userdata)
|
||||
local finalPoints, finalUserdata, finalEdges = _previousShapePoints, _previousShapeUserdata, _previousShapeEdges
|
||||
|
||||
for i = 2, #shapes.shapes do
|
||||
local shape = shapes.shapes[i]
|
||||
|
||||
_shapeToPointEdges(shape, _currentShapePoints, _currentShapeEdges, _currentShapeUserdata, userdata)
|
||||
|
||||
self.clipper:clip(
|
||||
clipper.union,
|
||||
_previousShapePoints, _previousShapeEdges,
|
||||
_currentShapePoints, _currentShapeEdges,
|
||||
nil,
|
||||
_previousShapeUserdata,
|
||||
_currentShapeUserdata,
|
||||
_nextShapePoints, _nextShapeEdges, _nextShapeUserdata)
|
||||
|
||||
finalPoints, finalUserdata, finalEdges = _nextShapePoints, _nextShapeUserdata, _nextShapeEdges
|
||||
|
||||
_previousShapePoints, _nextShapePoints = _nextShapePoints, _previousShapePoints
|
||||
_previousShapeEdges, _nextShapeEdges = _nextShapeEdges, _previousShapeEdges
|
||||
_previousShapeUserdata, _nextShapeUserdata = _nextShapeUserdata, _previousShapeUserdata
|
||||
end
|
||||
|
||||
local m = mesh.new(finalPoints, finalUserdata, finalEdges)
|
||||
self:addMesh(t, m)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param options slick.geometry.clipper.clipOptions?
|
||||
function navMeshBuilder:_prepareLayers(options)
|
||||
for _, layer in ipairs(self.layers) do
|
||||
local meshes = self.layerMeshes[layer.key]
|
||||
|
||||
if #meshes >= 1 then
|
||||
local currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = meshes[1].inputPoints, meshes[1].edges, meshes[1].inputUserdata, meshes[1].inputExteriorEdges, meshes[1].inputInteriorEdges
|
||||
|
||||
for i = 2, #meshes do
|
||||
currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.clipper:clip(
|
||||
clipper.union,
|
||||
currentPoints, { currentExteriorEdges, currentInteriorEdges },
|
||||
meshes[i].inputPoints, { meshes[i].inputExteriorEdges, meshes[i].inputInteriorEdges },
|
||||
options,
|
||||
currentUserdata,
|
||||
meshes[i].inputUserdata,
|
||||
{}, {}, {}, {}, {})
|
||||
end
|
||||
|
||||
layer.mesh = mesh.new(currentPoints, currentUserdata, currentExteriorEdges, currentInteriorEdges)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param options slick.geometry.clipper.clipOptions?
|
||||
function navMeshBuilder:_combineLayers(options)
|
||||
local currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.layers[1].mesh.inputPoints, self.layers[1].mesh.edges, self.layers[1].mesh.inputUserdata, self.layers[1].mesh.inputExteriorEdges, self.layers[1].mesh.inputInteriorEdges
|
||||
|
||||
for i = 2, #self.layers do
|
||||
local layer = self.layers[i]
|
||||
|
||||
local func
|
||||
if layer.combineMode == "union" then
|
||||
func = clipper.union
|
||||
elseif layer.combineMode == "difference" then
|
||||
func = clipper.difference
|
||||
end
|
||||
|
||||
if func and layer.mesh then
|
||||
currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.clipper:clip(
|
||||
func,
|
||||
currentPoints, { currentExteriorEdges, currentInteriorEdges },
|
||||
layer.mesh.inputPoints, { layer.mesh.inputExteriorEdges, layer.mesh.inputInteriorEdges },
|
||||
options,
|
||||
currentUserdata,
|
||||
layer.mesh.inputUserdata,
|
||||
{}, {}, {}, {}, {})
|
||||
end
|
||||
end
|
||||
|
||||
if #self.layers == 1 then
|
||||
currentPoints, currentEdges, currentUserdata = self.cache.triangulator:clean(currentPoints, currentEdges, currentUserdata, options)
|
||||
end
|
||||
|
||||
return currentPoints, currentExteriorEdges, currentUserdata
|
||||
end
|
||||
|
||||
--- @param options slick.geometry.clipper.clipOptions?
|
||||
function navMeshBuilder:build(options)
|
||||
self:_prepareLayers(options)
|
||||
|
||||
local points, edges, userdata = self:_combineLayers(options)
|
||||
local triangles = self.cache.triangulator:triangulate(points, edges, triangulationOptions)
|
||||
|
||||
return mesh.new(points, userdata, edges, triangles)
|
||||
end
|
||||
|
||||
return navMeshBuilder
|
||||
453
game/love_src/lib/slick/navigation/path.lua
Normal file
453
game/love_src/lib/slick/navigation/path.lua
Normal file
@ -0,0 +1,453 @@
|
||||
local point = require "slick.geometry.point"
|
||||
local segment = require "slick.geometry.segment"
|
||||
local edge = require "slick.navigation.edge"
|
||||
local vertex = require "slick.navigation.vertex"
|
||||
local slicktable = require "slick.util.slicktable"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
|
||||
--- @class slick.navigation.pathOptions
|
||||
--- @field optimize boolean?
|
||||
--- @field neighbor nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): boolean
|
||||
--- @field neighbors nil | fun(mesh: slick.navigation.mesh, triangle: slick.navigation.triangle): slick.navigation.triangle[] | nil
|
||||
--- @field distance nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): number
|
||||
--- @field heuristic nil | fun(triangle: slick.navigation.triangle, goalX: number, goalY: number): number
|
||||
--- @field visit nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): boolean?
|
||||
--- @field yield nil | fun(): boolean?
|
||||
local defaultPathOptions = {
|
||||
optimize = true
|
||||
}
|
||||
|
||||
--- @param from slick.navigation.triangle
|
||||
--- @param to slick.navigation.triangle
|
||||
--- @param e slick.navigation.edge
|
||||
--- @return boolean
|
||||
function defaultPathOptions.neighbor(from, to, e)
|
||||
return true
|
||||
end
|
||||
|
||||
--- @param mesh slick.navigation.mesh
|
||||
--- @param triangle slick.navigation.triangle
|
||||
--- @return slick.navigation.edge[] | nil
|
||||
function defaultPathOptions.neighbors(mesh, triangle)
|
||||
return mesh:getTriangleNeighbors(triangle.index)
|
||||
end
|
||||
|
||||
local _distanceEdgeSegment = segment.new()
|
||||
local _distanceEdgeCenter = point.new()
|
||||
--- @param from slick.navigation.triangle
|
||||
--- @param to slick.navigation.triangle
|
||||
--- @param e slick.navigation.edge
|
||||
function defaultPathOptions.distance(from, to, e)
|
||||
_distanceEdgeSegment:init(e.a.point, e.b.point)
|
||||
_distanceEdgeSegment:lerp(0.5, _distanceEdgeCenter)
|
||||
return from.centroid:distance(_distanceEdgeCenter) + to.centroid:distance(_distanceEdgeCenter)
|
||||
end
|
||||
|
||||
|
||||
--- @param triangle slick.navigation.triangle
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @return number
|
||||
function defaultPathOptions.heuristic(triangle, goalX, goalY)
|
||||
return math.sqrt((triangle.centroid.x - goalX) ^ 2 + (triangle.centroid.y - goalY) ^ 2)
|
||||
end
|
||||
|
||||
function defaultPathOptions.yield()
|
||||
-- Nothing.
|
||||
end
|
||||
|
||||
function defaultPathOptions.visit(triangle, e)
|
||||
-- Nothing.
|
||||
end
|
||||
|
||||
--- @class slick.navigation.impl.pathBehavior
|
||||
--- @field start slick.navigation.triangle | nil
|
||||
--- @field goal slick.navigation.triangle | nil
|
||||
local internalPathBehavior = {}
|
||||
|
||||
--- @class slick.navigation.path
|
||||
--- @field private options slick.navigation.pathOptions
|
||||
--- @field private behavior slick.navigation.impl.pathBehavior
|
||||
--- @field private fScores table<slick.navigation.triangle, number>
|
||||
--- @field private gScores table<slick.navigation.triangle, number>
|
||||
--- @field private hScores table<slick.navigation.triangle, number>
|
||||
--- @field private visitedEdges table<slick.navigation.edge, true>
|
||||
--- @field private visitedTriangles table<slick.navigation.triangle, true>
|
||||
--- @field private pending slick.navigation.triangle[]
|
||||
--- @field private closed slick.navigation.triangle[]
|
||||
--- @field private neighbors slick.navigation.triangle[]
|
||||
--- @field private graph table<slick.navigation.triangle, slick.navigation.triangle>
|
||||
--- @field private path slick.navigation.edge[]
|
||||
--- @field private portals slick.navigation.vertex[]
|
||||
--- @field private funnel slick.navigation.vertex[]
|
||||
--- @field private result slick.navigation.vertex[]
|
||||
--- @field private startVertex slick.navigation.vertex
|
||||
--- @field private goalVertex slick.navigation.vertex
|
||||
--- @field private startEdge slick.navigation.edge
|
||||
--- @field private goalEdge slick.navigation.edge
|
||||
--- @field private sharedStartGoalEdge slick.navigation.edge
|
||||
--- @field private _sortFScoreFunc fun(a: slick.navigation.triangle, b: slick.navigation.triangle): boolean
|
||||
--- @field private _sortHScoreFunc fun(a: slick.navigation.triangle, b: slick.navigation.triangle): boolean
|
||||
local path = {}
|
||||
local metatable = { __index = path }
|
||||
|
||||
--- @param options slick.navigation.pathOptions?
|
||||
function path.new(options)
|
||||
options = options or defaultPathOptions
|
||||
|
||||
local self = setmetatable({
|
||||
options = {
|
||||
optimize = options.optimize == nil and defaultPathOptions.optimize or not not options.optimize,
|
||||
neighbor = options.neighbor or defaultPathOptions.neighbor,
|
||||
neighbors = options.neighbors or defaultPathOptions.neighbors,
|
||||
distance = options.distance or defaultPathOptions.distance,
|
||||
heuristic = options.heuristic or defaultPathOptions.heuristic,
|
||||
visit = options.visit or defaultPathOptions.visit,
|
||||
yield = options.yield or defaultPathOptions.yield,
|
||||
},
|
||||
|
||||
behavior = {},
|
||||
|
||||
fScores = {},
|
||||
gScores = {},
|
||||
hScores = {},
|
||||
visitedEdges = {},
|
||||
visitedTriangles = {},
|
||||
pending = {},
|
||||
closed = {},
|
||||
neighbors = {},
|
||||
graph = {},
|
||||
path = {},
|
||||
portals = {},
|
||||
funnel = {},
|
||||
result = {},
|
||||
|
||||
startVertex = vertex.new(point.new(0, 0), nil, -1),
|
||||
goalVertex = vertex.new(point.new(0, 0), nil, -2),
|
||||
}, metatable)
|
||||
|
||||
self.startEdge = edge.new(self.startVertex, self.startVertex)
|
||||
self.goalEdge = edge.new(self.goalVertex, self.goalVertex)
|
||||
self.sharedStartGoalEdge = edge.new(self.startVertex, self.goalVertex)
|
||||
|
||||
function self._sortFScoreFunc(a, b)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
return (self.fScores[a] or math.huge) > (self.fScores[b] or math.huge)
|
||||
end
|
||||
|
||||
function self._sortHScoreFunc(a, b)
|
||||
--- @diagnostic disable-next-line: invisible
|
||||
return (self.hScores[a] or math.huge) > (self.hScores[b] or math.huge)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param mesh slick.navigation.mesh
|
||||
--- @param triangle slick.navigation.triangle
|
||||
--- @return slick.navigation.triangle[]
|
||||
function path:_neighbors(mesh, triangle)
|
||||
slicktable.clear(self.neighbors)
|
||||
|
||||
local neighbors = self.options.neighbors(mesh, triangle)
|
||||
if neighbors then
|
||||
for _, neighbor in ipairs(neighbors) do
|
||||
if self.options.neighbor(triangle, neighbor, mesh:getSharedTriangleEdge(triangle, neighbor)) then
|
||||
table.insert(self.neighbors, neighbor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self.neighbors
|
||||
end
|
||||
|
||||
--- @private
|
||||
function path:_reset()
|
||||
slicktable.clear(self.fScores)
|
||||
slicktable.clear(self.gScores)
|
||||
slicktable.clear(self.hScores)
|
||||
slicktable.clear(self.visitedEdges)
|
||||
slicktable.clear(self.visitedTriangles)
|
||||
slicktable.clear(self.pending)
|
||||
slicktable.clear(self.graph)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function path:_funnel()
|
||||
slicktable.clear(self.funnel)
|
||||
slicktable.clear(self.portals)
|
||||
|
||||
table.insert(self.portals, self.path[1].a)
|
||||
table.insert(self.portals, self.path[1].a)
|
||||
for i = 2, #self.path - 1 do
|
||||
local p = self.path[i]
|
||||
|
||||
local C, D = p.a, p.b
|
||||
local L, R = self.portals[#self.portals - 1], self.portals[#self.portals]
|
||||
|
||||
local sign = slickmath.direction(D.point, C.point, L.point, slickmath.EPSILON)
|
||||
sign = sign == 0 and slickmath.direction(D.point, C.point, R.point, slickmath.EPSILON) or sign
|
||||
|
||||
if sign > 0 then
|
||||
table.insert(self.portals, D)
|
||||
table.insert(self.portals, C)
|
||||
else
|
||||
table.insert(self.portals, C)
|
||||
table.insert(self.portals, D)
|
||||
end
|
||||
end
|
||||
table.insert(self.portals, self.path[#self.path].b)
|
||||
table.insert(self.portals, self.path[#self.path].b)
|
||||
|
||||
local apex, left, right = self.portals[1], self.portals[1], self.portals[2]
|
||||
local leftIndex, rightIndex = 1, 1
|
||||
|
||||
table.insert(self.funnel, apex)
|
||||
|
||||
local n = #self.portals / 2
|
||||
local index = 2
|
||||
while index <= n do
|
||||
local i = (index - 1) * 2 + 1
|
||||
local j = i + 1
|
||||
|
||||
local otherLeft = self.portals[i]
|
||||
local otherRight = self.portals[j]
|
||||
|
||||
local skip = false
|
||||
if slickmath.direction(right.point, otherRight.point, apex.point, slickmath.EPSILON) <= 0 then
|
||||
if apex.index == right.index or slickmath.direction(left.point, otherRight.point, apex.point, slickmath.EPSILON) > 0 then
|
||||
right = otherRight
|
||||
rightIndex = index
|
||||
else
|
||||
table.insert(self.funnel, left)
|
||||
apex = left
|
||||
right = left
|
||||
|
||||
rightIndex = leftIndex
|
||||
|
||||
index = leftIndex
|
||||
skip = true
|
||||
end
|
||||
end
|
||||
|
||||
if not skip and slickmath.direction(left.point, otherLeft.point, apex.point, slickmath.EPSILON) >= 0 then
|
||||
if apex.index == left.index or slickmath.direction(right.point, otherLeft.point, apex.point, slickmath.EPSILON) < 0 then
|
||||
left = otherLeft
|
||||
leftIndex = index
|
||||
else
|
||||
table.insert(self.funnel, right)
|
||||
apex = right
|
||||
left = right
|
||||
|
||||
leftIndex = rightIndex
|
||||
|
||||
index = rightIndex
|
||||
end
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
table.insert(self.funnel, self.portals[#self.portals])
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param mesh slick.navigation.mesh
|
||||
--- @param startX number
|
||||
--- @param startY number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param nearest boolean
|
||||
--- @param result number[]?
|
||||
--- @return number[]?, slick.navigation.vertex[]?
|
||||
function path:_find(mesh, startX, startY, goalX, goalY, nearest, result)
|
||||
self:_reset()
|
||||
|
||||
self.behavior.start = mesh:getContainingTriangle(startX, startY)
|
||||
self.behavior.goal = mesh:getContainingTriangle(goalX, goalY)
|
||||
|
||||
if not self.behavior.start then
|
||||
return nil
|
||||
end
|
||||
|
||||
self.fScores[self.behavior.start] = 0
|
||||
self.gScores[self.behavior.start] = 0
|
||||
|
||||
self.startVertex.point:init(startX, startY)
|
||||
self.goalVertex.point:init(goalX, goalY)
|
||||
|
||||
local pending = true
|
||||
local current = self.behavior.start
|
||||
while pending and current and current ~= self.behavior.goal do
|
||||
if current == self.behavior.start then
|
||||
self.visitedEdges[self.startEdge] = true
|
||||
else
|
||||
assert(current ~= self.behavior.goal, "cannot visit goal")
|
||||
assert(self.graph[current], "current has no previous")
|
||||
|
||||
local edge = mesh:getSharedTriangleEdge(self.graph[current], current)
|
||||
assert(edge, "missing edge between previous and current")
|
||||
|
||||
self.visitedEdges[edge] = true
|
||||
end
|
||||
|
||||
if not self.visitedTriangles[current] then
|
||||
table.insert(self.closed, current)
|
||||
self.visitedTriangles[current] = true
|
||||
end
|
||||
|
||||
for _, neighbor in ipairs(self:_neighbors(mesh, current)) do
|
||||
local edge = mesh:getSharedTriangleEdge(current, neighbor)
|
||||
if not self.visitedEdges[edge] then
|
||||
local continuePathfinding = self.options.visit(current, neighbor, edge)
|
||||
if continuePathfinding == false then
|
||||
pending = false
|
||||
break
|
||||
end
|
||||
|
||||
local distance = self.options.distance(current, neighbor, edge)
|
||||
local pendingGScore = (self.gScores[current] or math.huge) + distance
|
||||
if pendingGScore < (self.gScores[neighbor] or math.huge) then
|
||||
local heuristic = self.options.heuristic(neighbor, goalX, goalY)
|
||||
|
||||
self.graph[neighbor] = current
|
||||
self.gScores[neighbor] = pendingGScore
|
||||
self.hScores[neighbor] = heuristic
|
||||
self.fScores[neighbor] = pendingGScore + heuristic
|
||||
|
||||
table.insert(self.pending, neighbor)
|
||||
table.sort(self.pending, self._sortFScoreFunc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local continuePathfinding = self.options.yield()
|
||||
if continuePathfinding == false then
|
||||
pending = false
|
||||
end
|
||||
|
||||
current = table.remove(self.pending)
|
||||
end
|
||||
|
||||
local reachedGoal = current == self.behavior.goal and self.behavior.goal
|
||||
if not reachedGoal then
|
||||
if not nearest then
|
||||
return nil
|
||||
end
|
||||
|
||||
local bestTriangle = nil
|
||||
local bestHScore = math.huge
|
||||
for _, triangle in ipairs(self.closed) do
|
||||
local hScore = self.hScores[triangle]
|
||||
if hScore and hScore < bestHScore then
|
||||
bestHScore = hScore
|
||||
bestTriangle = triangle
|
||||
end
|
||||
end
|
||||
|
||||
if not bestTriangle then
|
||||
return nil
|
||||
end
|
||||
|
||||
current = bestTriangle
|
||||
end
|
||||
|
||||
slicktable.clear(self.path)
|
||||
while current do
|
||||
local next = self.graph[current]
|
||||
if next then
|
||||
table.insert(self.path, 1, mesh:getSharedTriangleEdge(current, next))
|
||||
end
|
||||
|
||||
current = self.graph[current]
|
||||
end
|
||||
|
||||
if #self.path == 0 then
|
||||
self.startVertex.point:init(startX, startY)
|
||||
self.goalVertex.point:init(goalX, goalY)
|
||||
|
||||
table.insert(self.path, self.sharedStartGoalEdge)
|
||||
else
|
||||
self.startEdge.a.point:init(startX, startY)
|
||||
self.startEdge.b = self.path[1].a
|
||||
self.startEdge.min = math.min(self.startEdge.a.index, self.startEdge.b.index)
|
||||
self.startEdge.max = math.max(self.startEdge.a.index, self.startEdge.b.index)
|
||||
|
||||
if self.startEdge.a.point:distance(self.startEdge.b.point) > slickmath.EPSILON then
|
||||
table.insert(self.path, 1, self.startEdge)
|
||||
end
|
||||
|
||||
if reachedGoal then
|
||||
self.goalEdge.a = self.path[#self.path].b
|
||||
self.goalEdge.b.point:init(goalX, goalY)
|
||||
self.goalEdge.min = math.min(self.goalEdge.a.index, self.goalEdge.b.index)
|
||||
self.goalEdge.max = math.max(self.goalEdge.a.index, self.goalEdge.b.index)
|
||||
|
||||
if self.goalEdge.a.point:distance(self.goalEdge.b.point) > slickmath.EPSILON then
|
||||
table.insert(self.path, self.goalEdge)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @type slick.navigation.vertex[]
|
||||
local path
|
||||
if self.options.optimize and #self.path > 1 then
|
||||
self:_funnel()
|
||||
path = self.funnel
|
||||
else
|
||||
slicktable.clear(self.result)
|
||||
if #self.path == 1 then
|
||||
local p = self.path[1]
|
||||
table.insert(self.result, p.a)
|
||||
table.insert(self.result, p.b)
|
||||
else
|
||||
table.insert(self.result, self.path[1].a)
|
||||
|
||||
for i = 1, #self.path - 1 do
|
||||
local p1 = self.path[i]
|
||||
local p2 = self.path[i + 1]
|
||||
|
||||
if p1.b.index == p2.a.index then
|
||||
table.insert(self.result, p1.b)
|
||||
elseif p1.a.index == p2.b.index then
|
||||
table.insert(self.result, p1.a)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(self.result, self.path[#self.path].b)
|
||||
end
|
||||
path = self.result
|
||||
end
|
||||
|
||||
result = result or {}
|
||||
slicktable.clear(result)
|
||||
for _, vertex in ipairs(path) do
|
||||
table.insert(result, vertex.point.x)
|
||||
table.insert(result, vertex.point.y)
|
||||
end
|
||||
|
||||
return result, path
|
||||
end
|
||||
|
||||
--- @param mesh slick.navigation.mesh
|
||||
--- @param startX number
|
||||
--- @param startY number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @return number[]?, slick.navigation.vertex[]?
|
||||
function path:find(mesh, startX, startY, goalX, goalY)
|
||||
return self:_find(mesh, startX, startY, goalX, goalY, false)
|
||||
end
|
||||
|
||||
--- @param mesh slick.navigation.mesh
|
||||
--- @param startX number
|
||||
--- @param startY number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @return number[]?, slick.navigation.vertex[]?
|
||||
function path:nearest(mesh, startX, startY, goalX, goalY)
|
||||
return self:_find(mesh, startX, startY, goalX, goalY, true)
|
||||
end
|
||||
|
||||
return path
|
||||
38
game/love_src/lib/slick/navigation/triangle.lua
Normal file
38
game/love_src/lib/slick/navigation/triangle.lua
Normal file
@ -0,0 +1,38 @@
|
||||
local point = require "slick.geometry.point"
|
||||
local rectangle = require "slick.geometry.rectangle"
|
||||
|
||||
--- @class slick.navigation.triangle
|
||||
--- @field triangle slick.navigation.vertex[]
|
||||
--- @field vertices table<number, slick.navigation.vertex>
|
||||
--- @field bounds slick.geometry.rectangle
|
||||
--- @field centroid slick.geometry.point
|
||||
--- @field index number
|
||||
local triangle = {}
|
||||
local metatable = { __index = triangle }
|
||||
|
||||
--- @param a slick.navigation.vertex
|
||||
--- @param b slick.navigation.vertex
|
||||
--- @param c slick.navigation.vertex
|
||||
--- @param index number
|
||||
function triangle.new(a, b, c, index)
|
||||
local self = setmetatable({
|
||||
triangle = { a, b, c },
|
||||
vertices = {
|
||||
[a.index] = a,
|
||||
[b.index] = b,
|
||||
[c.index] = c
|
||||
},
|
||||
centroid = point.new((a.point.x + b.point.x + c.point.x) / 3, (a.point.y + b.point.y + c.point.y) / 3),
|
||||
bounds = rectangle.new(a.point.x, a.point.y),
|
||||
index = index
|
||||
}, metatable)
|
||||
|
||||
for i = 2, #self.triangle do
|
||||
local p = self.triangle[i].point
|
||||
self.bounds:expand(p.x, p.y)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return triangle
|
||||
19
game/love_src/lib/slick/navigation/vertex.lua
Normal file
19
game/love_src/lib/slick/navigation/vertex.lua
Normal file
@ -0,0 +1,19 @@
|
||||
--- @class slick.navigation.vertex
|
||||
--- @field point slick.geometry.point
|
||||
--- @field userdata any
|
||||
--- @field index number
|
||||
local vertex = {}
|
||||
local metatable = { __index = vertex }
|
||||
|
||||
--- @param point slick.geometry.point
|
||||
--- @param userdata any
|
||||
--- @param index number
|
||||
function vertex.new(point, userdata, index)
|
||||
return setmetatable({
|
||||
point = point,
|
||||
userdata = userdata,
|
||||
index = index
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
return vertex
|
||||
36
game/love_src/lib/slick/options.lua
Normal file
36
game/love_src/lib/slick/options.lua
Normal file
@ -0,0 +1,36 @@
|
||||
--- @class slick.options
|
||||
--- @field epsilon number?
|
||||
--- @field maxBounces number?
|
||||
--- @field maxJitter number?
|
||||
--- @field debug boolean?
|
||||
--- @field quadTreeX number?
|
||||
--- @field quadTreeY number?
|
||||
--- @field quadTreeMaxLevels number?
|
||||
--- @field quadTreeMaxData number?
|
||||
--- @field quadTreeExpand boolean?
|
||||
--- @field quadTreeOptimizationMargin number?
|
||||
--- @field sharedCache slick.cache?
|
||||
local defaultOptions = {
|
||||
debug = false,
|
||||
|
||||
maxBounces = 4,
|
||||
maxJitter = 1,
|
||||
|
||||
quadTreeMaxLevels = 8,
|
||||
quadTreeMaxData = 8,
|
||||
quadTreeExpand = true,
|
||||
quadTreeOptimizationMargin = 0.25
|
||||
}
|
||||
|
||||
--- @type slick.options
|
||||
local defaultOptionsWrapper = setmetatable(
|
||||
{},
|
||||
{
|
||||
__metatable = true,
|
||||
__index = defaultOptions,
|
||||
__newindex = function()
|
||||
error("default options is immutable", 2)
|
||||
end
|
||||
})
|
||||
|
||||
return defaultOptionsWrapper
|
||||
267
game/love_src/lib/slick/responses.lua
Normal file
267
game/love_src/lib/slick/responses.lua
Normal file
@ -0,0 +1,267 @@
|
||||
local point = require "slick.geometry.point"
|
||||
local worldQuery = require "slick.worldQuery"
|
||||
local slickmath = require "slick.util.slickmath"
|
||||
|
||||
local _workingQueries = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function getWorkingQuery(world)
|
||||
local workingQuery = _workingQueries[world]
|
||||
if not _workingQueries[world] then
|
||||
workingQuery = worldQuery.new(world)
|
||||
_workingQueries[world] = workingQuery
|
||||
end
|
||||
|
||||
return workingQuery
|
||||
end
|
||||
|
||||
local _cachedSlideNormal = point.new()
|
||||
local _cachedSlideCurrentPosition = point.new()
|
||||
local _cachedSlideTouchPosition = point.new()
|
||||
local _cachedSlideGoalPosition = point.new()
|
||||
local _cachedSlideGoalDirection = point.new()
|
||||
local _cachedSlideNewGoalPosition = point.new()
|
||||
local _cachedSlideDirection = point.new()
|
||||
|
||||
local function trySlide(normalX, normalY, touchX, touchY, x, y, goalX, goalY)
|
||||
_cachedSlideCurrentPosition:init(x, y)
|
||||
_cachedSlideTouchPosition:init(touchX, touchY)
|
||||
_cachedSlideGoalPosition:init(goalX, goalY)
|
||||
|
||||
_cachedSlideNormal:init(normalX, normalY)
|
||||
_cachedSlideNormal:left(_cachedSlideGoalDirection)
|
||||
|
||||
_cachedSlideCurrentPosition:direction(_cachedSlideGoalPosition, _cachedSlideNewGoalPosition)
|
||||
_cachedSlideNewGoalPosition:normalize(_cachedSlideDirection)
|
||||
|
||||
local goalDotDirection = _cachedSlideNewGoalPosition:dot(_cachedSlideGoalDirection)
|
||||
_cachedSlideGoalDirection:multiplyScalar(goalDotDirection, _cachedSlideGoalDirection)
|
||||
_cachedSlideTouchPosition:add(_cachedSlideGoalDirection, _cachedSlideNewGoalPosition)
|
||||
|
||||
return _cachedSlideNewGoalPosition.x, _cachedSlideNewGoalPosition.y
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param query slick.worldQuery
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @return boolean
|
||||
local function findDidSlide(world, response, query, x, y, goalX, goalY)
|
||||
if #query.results == 0 or query.results[1].time > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local didSlide = true
|
||||
for _, otherResponse in ipairs(query.results) do
|
||||
if otherResponse.time > 0 then
|
||||
didSlide = false
|
||||
break
|
||||
end
|
||||
|
||||
local otherResponseName = world:respond(otherResponse, query, x, y, goalX, goalY, true)
|
||||
if (otherResponse.shape == response.shape and otherResponse.otherShape == response.otherShape) or otherResponseName == "slide" then
|
||||
didSlide = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return didSlide
|
||||
end
|
||||
|
||||
local function didMove(x, y, goalX, goalY)
|
||||
return not (x == goalX and y == goalY)
|
||||
end
|
||||
|
||||
local _slideNormals = {}
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param query slick.worldQuery
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc
|
||||
--- @param result slick.worldQuery
|
||||
--- @return number, number, number, number, string?, slick.worldQueryResponse?
|
||||
local function slide(world, query, response, x, y, goalX, goalY, filter, result)
|
||||
result:push(response)
|
||||
|
||||
local touchX, touchY = response.touch.x, response.touch.y
|
||||
local newGoalX, newGoalY = goalX, goalY
|
||||
|
||||
local index = query:getResponseIndex(response)
|
||||
local didSlide = false
|
||||
local q = getWorkingQuery(world)
|
||||
|
||||
_slideNormals[1], _slideNormals[2] = response.normals, response.alternateNormals
|
||||
|
||||
for _, normals in ipairs(_slideNormals) do
|
||||
for _, normal in ipairs(normals) do
|
||||
local workingGoalX, workingGoalY = trySlide(normal.x, normal.y, response.touch.x, response.touch.y, x, y, goalX, goalY)
|
||||
if didMove(response.touch.x, response.touch.y, workingGoalX, workingGoalY) then
|
||||
world:project(response.item, response.touch.x, response.touch.y, workingGoalX, workingGoalY, filter, q)
|
||||
didSlide = findDidSlide(world, response, q, response.touch.x, response.touch.y, workingGoalX, workingGoalY)
|
||||
|
||||
if didSlide then
|
||||
newGoalX = workingGoalX
|
||||
newGoalY = workingGoalY
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if didSlide then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if didSlide then
|
||||
for i = index + 1, #query.results do
|
||||
local otherResponse = query.results[i]
|
||||
if not slickmath.lessThanEqual(otherResponse.time, response.time) then
|
||||
break
|
||||
end
|
||||
|
||||
world:respond(otherResponse, query, touchX, touchY, newGoalX, newGoalY, false)
|
||||
result:push(otherResponse)
|
||||
end
|
||||
|
||||
world:project(response.item, touchX, touchY, newGoalX, newGoalY, filter, query)
|
||||
return touchX, touchY, newGoalX, newGoalY, nil, query.results[1]
|
||||
else
|
||||
local shape, otherShape = response.shape, response.otherShape
|
||||
world:project(response.item, touchX, touchY, goalX, goalY, filter, query)
|
||||
|
||||
--- @cast otherShape slick.collision.shape
|
||||
local index = query:getShapeResponseIndex(shape, otherShape)
|
||||
local nextIndex = index and (index + 1) or 1
|
||||
|
||||
--- @type slick.worldQueryResponse?
|
||||
local nextResponse = query.results[nextIndex]
|
||||
|
||||
if index and nextResponse then
|
||||
local currentResponse = query.results[index]
|
||||
if nextResponse.time > currentResponse.time then
|
||||
nextResponse = nil
|
||||
end
|
||||
end
|
||||
|
||||
return touchX, touchY, goalX, goalY, nil, nextResponse
|
||||
end
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param query slick.worldQuery
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc
|
||||
--- @param result slick.worldQuery
|
||||
--- @return number, number, number, number, string?, slick.worldQueryResponse?
|
||||
local function touch(world, query, response, x, y, goalX, goalY, filter, result)
|
||||
result:push(response)
|
||||
|
||||
local touchX, touchY = response.touch.x, response.touch.y
|
||||
|
||||
local index = query:getResponseIndex(response)
|
||||
local nextResponse = query.results[index + 1]
|
||||
return touchX, touchY, touchX, touchY, nil, nextResponse
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param query slick.worldQuery
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc
|
||||
--- @param result slick.worldQuery
|
||||
--- @return number, number, number, number, string?, slick.worldQueryResponse?
|
||||
local function cross(world, query, response, x, y, goalX, goalY, filter, result)
|
||||
result:push(response)
|
||||
|
||||
local index = query:getResponseIndex(response)
|
||||
local nextResponse = query.results[index + 1]
|
||||
|
||||
if not nextResponse then
|
||||
return goalX, goalY, goalX, goalY, nil, nil
|
||||
end
|
||||
|
||||
return nextResponse.touch.x, nextResponse.touch.y, goalX, goalY, nil, nextResponse
|
||||
end
|
||||
|
||||
local _cachedBounceCurrentPosition = point.new()
|
||||
local _cachedBounceTouchPosition = point.new()
|
||||
local _cachedBounceGoalPosition = point.new()
|
||||
local _cachedBounceNormal = point.new()
|
||||
local _cachedBounceNewGoalPosition = point.new()
|
||||
local _cachedBounceDirection = point.new()
|
||||
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
local function getBounceNormal(response, x, y, goalX, goalY)
|
||||
_cachedBounceCurrentPosition:init(x, y)
|
||||
_cachedBounceTouchPosition:init(response.touch.x, response.touch.y)
|
||||
_cachedBounceGoalPosition:init(goalX, goalY)
|
||||
|
||||
_cachedBounceCurrentPosition:direction(_cachedBounceGoalPosition, _cachedBounceDirection)
|
||||
_cachedBounceDirection:normalize(_cachedBounceDirection)
|
||||
|
||||
local bounceNormalDot = 2 * response.normal:dot(_cachedBounceDirection)
|
||||
response.normal:multiplyScalar(bounceNormalDot, _cachedBounceNormal)
|
||||
_cachedBounceDirection:sub(_cachedBounceNormal, _cachedBounceNormal)
|
||||
_cachedBounceNormal:normalize(_cachedBounceNormal)
|
||||
|
||||
if _cachedBounceNormal:lengthSquared() == 0 then
|
||||
response.normal:negate(_cachedBounceNormal)
|
||||
end
|
||||
|
||||
return _cachedBounceNormal
|
||||
end
|
||||
|
||||
--- @param world slick.world
|
||||
--- @param query slick.worldQuery
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc
|
||||
--- @param result slick.worldQuery
|
||||
--- @return number, number, number, number, string?, slick.worldQueryResponse?
|
||||
local function bounce(world, query, response, x, y, goalX, goalY, filter, result)
|
||||
local bounceNormal = getBounceNormal(response, x, y, goalX, goalY)
|
||||
|
||||
local maxDistance = _cachedBounceCurrentPosition:distance(_cachedBounceGoalPosition)
|
||||
local currentDistance = _cachedBounceCurrentPosition:distance(_cachedBounceTouchPosition)
|
||||
local remainingDistance = maxDistance - currentDistance
|
||||
|
||||
bounceNormal:multiplyScalar(remainingDistance, _cachedBounceNewGoalPosition)
|
||||
_cachedBounceNewGoalPosition:add(_cachedBounceTouchPosition, _cachedBounceNewGoalPosition)
|
||||
|
||||
local newGoalX = _cachedBounceNewGoalPosition.x
|
||||
local newGoalY = _cachedBounceNewGoalPosition.y
|
||||
local touchX, touchY = response.touch.x, response.touch.y
|
||||
|
||||
response.extra.bounceNormal = query:allocate(point, bounceNormal.x, bounceNormal.y)
|
||||
result:push(response, false)
|
||||
|
||||
world:project(response.item, touchX, touchY, newGoalX, newGoalY, filter, query)
|
||||
return touchX, touchY, newGoalX, newGoalY, nil, query.results[1]
|
||||
end
|
||||
|
||||
return {
|
||||
slide = slide,
|
||||
touch = touch,
|
||||
cross = cross,
|
||||
bounce = bounce
|
||||
}
|
||||
209
game/love_src/lib/slick/shape.lua
Normal file
209
game/love_src/lib/slick/shape.lua
Normal file
@ -0,0 +1,209 @@
|
||||
local box = require("slick.collision.box")
|
||||
local lineSegment = require("slick.collision.lineSegment")
|
||||
local polygon = require("slick.collision.polygon")
|
||||
local polygonMesh = require("slick.collision.polygonMesh")
|
||||
local shapeGroup = require("slick.collision.shapeGroup")
|
||||
local tag = require("slick.tag")
|
||||
local enum = require("slick.enum")
|
||||
local util = require("slick.util")
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param w number
|
||||
--- @param h number
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newRectangle(x, y, w, h, tag)
|
||||
return {
|
||||
type = box,
|
||||
n = 4,
|
||||
tag = tag,
|
||||
arguments = { x, y, w, h }
|
||||
}
|
||||
end
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param radius number
|
||||
--- @param segments number?
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newCircle(x, y, radius, segments, tag)
|
||||
local points = segments or math.max(math.floor(math.sqrt(radius * 20)), 8)
|
||||
local vertices = {}
|
||||
local angleStep = (2 * math.pi) / points
|
||||
|
||||
for angle = 0, 2 * math.pi - angleStep, angleStep do
|
||||
table.insert(vertices, x + radius * math.cos(angle))
|
||||
table.insert(vertices, y + radius * math.sin(angle))
|
||||
end
|
||||
|
||||
return {
|
||||
type = polygon,
|
||||
n = #vertices,
|
||||
tag = tag,
|
||||
arguments = vertices
|
||||
}
|
||||
end
|
||||
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newLineSegment(x1, y1, x2, y2, tag)
|
||||
return {
|
||||
type = lineSegment,
|
||||
n = 4,
|
||||
tag = tag,
|
||||
arguments = { x1, y1, x2, y2 }
|
||||
}
|
||||
end
|
||||
|
||||
local function _newChainHelper(points, i, j)
|
||||
local length = #points / 2
|
||||
|
||||
i = i or 1
|
||||
j = j or length
|
||||
|
||||
local k = (i % length) + 1
|
||||
local x1, y1 = points[(i - 1) * 2 + 1], points[(i - 1) * 2 + 2]
|
||||
local x2, y2 = points[(k - 1) * 2 + 1], points[(k - 1) * 2 + 2]
|
||||
if i == j then
|
||||
return newLineSegment(x1, y1, x2, y2)
|
||||
else
|
||||
return newLineSegment(x1, y1, x2, y2), _newChainHelper(points, i + 1, j)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param points number[] an array of points in the form { x1, y2, x2, y2, ... }
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newChain(points, tag)
|
||||
assert(#points % 2 == 0, "expected a list of (x, y) tuples")
|
||||
assert(#points >= 6, "expected a minimum of 3 points")
|
||||
|
||||
return {
|
||||
type = shapeGroup,
|
||||
n = #points / 2,
|
||||
tag = tag,
|
||||
arguments = { _newChainHelper(points) }
|
||||
}
|
||||
end
|
||||
|
||||
local function _newPolylineHelper(segments, i, j)
|
||||
i = i or 1
|
||||
j = j or #segments
|
||||
|
||||
if i == j then
|
||||
return newLineSegment(unpack(segments[i]))
|
||||
else
|
||||
return newLineSegment(unpack(segments[i])), _newPolylineHelper(segments, i + 1, j)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param segments number[][] an array of segments in the form { { x1, y1, x2, y2 }, { x1, y1, x2, y2 }, ... }
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newPolyline(segments, tag)
|
||||
return {
|
||||
type = shapeGroup,
|
||||
n = #segments,
|
||||
tag = tag,
|
||||
arguments = { _newPolylineHelper(segments) }
|
||||
}
|
||||
end
|
||||
|
||||
--- @param vertices number[] a list of x, y coordinates in the form `{ x1, y1, x2, y2, ..., xn, yn }`
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newPolygon(vertices, tag)
|
||||
return {
|
||||
type = polygon,
|
||||
n = #vertices,
|
||||
tag = tag,
|
||||
arguments = { unpack(vertices) }
|
||||
}
|
||||
end
|
||||
|
||||
--- @param ... any
|
||||
--- @return number, slick.tag?
|
||||
local function _getTagAndCount(...)
|
||||
local n = select("#", ...)
|
||||
|
||||
local maybeTag = select(select("#", ...), ...)
|
||||
if util.is(maybeTag, tag) or util.is(maybeTag, enum) then
|
||||
return n - 1, maybeTag
|
||||
end
|
||||
|
||||
return n, nil
|
||||
end
|
||||
|
||||
--- @param ... number[] | slick.tag | slick.enum a list of x, y coordinates in the form `{ x1, y1, x2, y2, ..., xn, yn }`
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newPolygonMesh(...)
|
||||
local n, tag = _getTagAndCount(...)
|
||||
|
||||
return {
|
||||
type = polygonMesh,
|
||||
n = n,
|
||||
tag = tag,
|
||||
arguments = { ... }
|
||||
}
|
||||
end
|
||||
|
||||
local function _newMeshHelper(polygons, i, j)
|
||||
i = i or 1
|
||||
j = j or #polygons
|
||||
|
||||
if i == j then
|
||||
return newPolygon(polygons[i])
|
||||
else
|
||||
return newPolygon(polygons[i]), _newMeshHelper(polygons, i + 1, j)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param polygons number[][] an array of segments in the form { { x1, y1, x2, y2, x3, y3, ..., xn, yn }, ... }
|
||||
--- @param tag slick.tag | slick.enum | nil
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newMesh(polygons, tag)
|
||||
return {
|
||||
type = shapeGroup,
|
||||
n = #polygons,
|
||||
tag = tag,
|
||||
arguments = { _newMeshHelper(polygons) }
|
||||
}
|
||||
end
|
||||
|
||||
--- @alias slick.collision.shapeDefinition {
|
||||
--- type: { new: fun(entity: slick.entity | slick.cache, ...: any): slick.collision.shapelike },
|
||||
--- n: number,
|
||||
--- tag: slick.tag?,
|
||||
--- arguments: table,
|
||||
--- }
|
||||
|
||||
--- @param ... slick.collision.shapeDefinition | slick.tag
|
||||
--- @return slick.collision.shapeDefinition
|
||||
local function newShapeGroup(...)
|
||||
local n, tag = _getTagAndCount(...)
|
||||
|
||||
return {
|
||||
type = shapeGroup,
|
||||
n = n,
|
||||
tag = tag,
|
||||
arguments = { ... }
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
newRectangle = newRectangle,
|
||||
newCircle = newCircle,
|
||||
newLineSegment = newLineSegment,
|
||||
newChain = newChain,
|
||||
newPolyline = newPolyline,
|
||||
newPolygon = newPolygon,
|
||||
newPolygonMesh = newPolygonMesh,
|
||||
newMesh = newMesh,
|
||||
newShapeGroup = newShapeGroup,
|
||||
}
|
||||
12
game/love_src/lib/slick/tag.lua
Normal file
12
game/love_src/lib/slick/tag.lua
Normal file
@ -0,0 +1,12 @@
|
||||
--- @class slick.tag
|
||||
--- @field value any
|
||||
local tag = {}
|
||||
local metatable = { __index = tag }
|
||||
|
||||
function tag.new(value)
|
||||
return setmetatable({
|
||||
value = value
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
return tag
|
||||
9
game/love_src/lib/slick/util/common.lua
Normal file
9
game/love_src/lib/slick/util/common.lua
Normal file
@ -0,0 +1,9 @@
|
||||
return {
|
||||
is = function(obj, t)
|
||||
return type(t) == "table" and type(obj) == "table" and getmetatable(obj) and getmetatable(obj).__index == t
|
||||
end,
|
||||
|
||||
type = function(obj)
|
||||
return type(obj) == "table" and getmetatable(obj) and getmetatable(obj).__index
|
||||
end
|
||||
}
|
||||
11
game/love_src/lib/slick/util/init.lua
Normal file
11
game/love_src/lib/slick/util/init.lua
Normal file
@ -0,0 +1,11 @@
|
||||
local common = require("slick.util.common")
|
||||
|
||||
return {
|
||||
math = require("slick.util.slickmath"),
|
||||
pool = require("slick.util.pool"),
|
||||
search = require("slick.util.search"),
|
||||
table = require("slick.util.slicktable"),
|
||||
|
||||
is = common.is,
|
||||
type = common.type
|
||||
}
|
||||
106
game/love_src/lib/slick/util/pool.lua
Normal file
106
game/love_src/lib/slick/util/pool.lua
Normal file
@ -0,0 +1,106 @@
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
local util = require("slick.util.common")
|
||||
|
||||
--- @class slick.util.pool
|
||||
--- @field type { new: function }
|
||||
--- @field used table
|
||||
--- @field free table
|
||||
local pool = {}
|
||||
local metatable = { __index = pool }
|
||||
|
||||
--- Constructs a new pool for the provided type.
|
||||
--- @param poolType any
|
||||
--- @return slick.util.pool
|
||||
function pool.new(poolType)
|
||||
return setmetatable({
|
||||
type = poolType,
|
||||
used = {},
|
||||
free = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- Removes `value` from this pool's scope. `value` will not be re-used.
|
||||
--- Only removing allocated (**not free!**) values is permitted. If `value` is in the free list,
|
||||
--- this will fail and return false.
|
||||
--- @param value any
|
||||
--- @return boolean result true if `value` was removed from this pool, false otherwise
|
||||
function pool:remove(value)
|
||||
if self.used[value] then
|
||||
self.used[value] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Adds `value` to this pool's scope. Behavior is undefined if `value` belongs to another pool.
|
||||
--- If `value` is not exactly of the type this pool manages then it will not be added to this pool.
|
||||
--- @param value any
|
||||
--- @return boolean result true if `value` was added to this pool, false otherwise
|
||||
function pool:add(value)
|
||||
if util.is(value, self.type) then
|
||||
self.used[value] = true
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Moves `value` from the source pool to the target pool.
|
||||
--- This effective removes `value` from `source` and adds `value` to `target`
|
||||
--- @param source slick.util.pool
|
||||
--- @param target slick.util.pool
|
||||
--- @param value any
|
||||
--- @return boolean result true if `value` was moved successfully, `false` otherwise
|
||||
--- @see slick.util.pool.add
|
||||
--- @see slick.util.pool.remove
|
||||
function pool.swap(source, target, value)
|
||||
return source:remove(value) and target:add(value)
|
||||
end
|
||||
|
||||
--- Allocates a new type, initializing the new instance with the provided arguments.
|
||||
--- @param ... any arguments to pass to the new instance
|
||||
--- @return any
|
||||
function pool:allocate(...)
|
||||
local result
|
||||
if #self.free == 0 then
|
||||
result = self.type and self.type.new() or {}
|
||||
if self.type then
|
||||
result:init(...)
|
||||
end
|
||||
|
||||
self.used[result] = true
|
||||
else
|
||||
result = table.remove(self.free, #self.free)
|
||||
if self.type then
|
||||
result:init(...)
|
||||
end
|
||||
|
||||
self.used[result] = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Returns an instance to the pool.
|
||||
--- @param t any the type to return to the pool
|
||||
function pool:deallocate(t)
|
||||
self.used[t] = nil
|
||||
table.insert(self.free, t)
|
||||
end
|
||||
|
||||
--- Moves all used instances to the free instance list.
|
||||
--- Anything returned by allocate is no longer considered valid - the instance may be reused.
|
||||
--- @see slick.util.pool.allocate
|
||||
function pool:reset()
|
||||
for v in pairs(self.used) do
|
||||
self:deallocate(v)
|
||||
end
|
||||
end
|
||||
|
||||
--- Clears all tracking for free and used instances.
|
||||
function pool:clear()
|
||||
slicktable.clear(self.used)
|
||||
slicktable.clear(self.free)
|
||||
end
|
||||
|
||||
return pool
|
||||
147
game/love_src/lib/slick/util/search.lua
Normal file
147
game/love_src/lib/slick/util/search.lua
Normal file
@ -0,0 +1,147 @@
|
||||
local search = {}
|
||||
|
||||
--- A result from a compare function.
|
||||
--- A value compare than one means compare, zero means equal, and greater than one means greater
|
||||
--- when comparing 'a' to 'b' (in that order).
|
||||
---@alias slick.util.search.compareResult -1 | 0 | 1
|
||||
|
||||
--- A compare function to be used in a binary search.
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @alias slick.util.search.compareFunc fun(a: T, b: O): slick.util.search.compareResult
|
||||
|
||||
--- Finds the first value equal to `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number?
|
||||
function search.first(array, value, compare, start, stop)
|
||||
local result = search.lessThanEqual(array, value, compare, start, stop)
|
||||
if result >= (start or 1) and result <= (stop or #array) and compare(array[result], value) == 0 then
|
||||
return result
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Finds the last value equal to `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number?
|
||||
function search.last(array, value, compare, start, stop)
|
||||
local result = search.greaterThanEqual(array, value, compare, start, stop)
|
||||
if result >= (start or 1) and result <= (stop or #array) and compare(array[result], value) == 0 then
|
||||
return result
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Finds the first value less than `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number
|
||||
function search.lessThan(array, value, compare, start, stop)
|
||||
start = start or 1
|
||||
stop = stop or #array
|
||||
|
||||
local result = start - 1
|
||||
while start <= stop do
|
||||
local midPoint = math.floor((start + stop + 1) / 2)
|
||||
if compare(array[midPoint], value) < 0 then
|
||||
result = midPoint
|
||||
start = midPoint + 1
|
||||
else
|
||||
stop = midPoint - 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Finds the first value less than or equal to `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number
|
||||
function search.lessThanEqual(array, value, compare, start, stop)
|
||||
local result = search.lessThan(array, value, compare, start, stop)
|
||||
if result < (stop or #array) then
|
||||
if compare(array[result + 1], value) == 0 then
|
||||
result = result + 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Finds the first value less greater than `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number
|
||||
function search.greaterThan(array, value, compare, start, stop)
|
||||
local start = start or 1
|
||||
local stop = stop or #array
|
||||
|
||||
local result = stop + 1
|
||||
while start <= stop do
|
||||
local midPoint = math.floor((start + stop + 1) / 2)
|
||||
if compare(array[midPoint], value) > 0 then
|
||||
result = midPoint
|
||||
stop = midPoint - 1
|
||||
else
|
||||
start = midPoint + 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Finds the first value greater than or equal to `value` and returns the index of that value
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @param array T[]
|
||||
--- @param value T
|
||||
--- @param compare slick.util.search.compareFunc
|
||||
--- @param start number?
|
||||
--- @param stop number?
|
||||
--- @return number
|
||||
function search.greaterThanEqual(array, value, compare, start, stop)
|
||||
local result = search.greaterThan(array, value, compare, start, stop)
|
||||
if result > (start or 1) then
|
||||
if compare(array[result - 1], value) == 0 then
|
||||
result = result - 1
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @generic T
|
||||
--- @generic O
|
||||
--- @alias slick.util.search.searchFunc fun(array: T[], value: O, compare: slick.util.search.compareFunc, start: number?, stop: number?)
|
||||
|
||||
return search
|
||||
324
game/love_src/lib/slick/util/slickmath.lua
Normal file
324
game/love_src/lib/slick/util/slickmath.lua
Normal file
@ -0,0 +1,324 @@
|
||||
local slickmath = {}
|
||||
|
||||
slickmath.EPSILON = 1e-5
|
||||
|
||||
--- @param value number
|
||||
--- @param increment number
|
||||
--- @param max number
|
||||
--- @return number
|
||||
function slickmath.wrap(value, increment, max)
|
||||
return (value + increment - 1) % max + 1
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param c slick.geometry.point
|
||||
--- @return number
|
||||
function slickmath.angle(a, b, c)
|
||||
local abx = a.x - b.x
|
||||
local aby = a.y - b.y
|
||||
local cbx = c.x - b.x
|
||||
local cby = c.y - b.y
|
||||
|
||||
local abLength = math.sqrt(abx ^ 2 + aby ^ 2)
|
||||
local cbLength = math.sqrt(cbx ^ 2 + cby ^ 2)
|
||||
|
||||
if abLength == 0 or cbLength == 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
local abNormalX = abx / abLength
|
||||
local abNormalY = aby / abLength
|
||||
local cbNormalX = cbx / cbLength
|
||||
local cbNormalY = cby / cbLength
|
||||
|
||||
local dot = abNormalX * cbNormalX + abNormalY * cbNormalY
|
||||
if not (dot >= -1 and dot <= 1) then
|
||||
return 0
|
||||
end
|
||||
|
||||
return math.acos(dot)
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @return number
|
||||
function slickmath.cross(a, b, c)
|
||||
local left = (a.y - c.y) * (b.x - c.x)
|
||||
local right = (a.x - c.x) * (b.y - c.y)
|
||||
|
||||
return left - right
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param c slick.geometry.point
|
||||
--- @param E number?
|
||||
--- @return -1 | 0 | 1
|
||||
function slickmath.direction(a, b, c, E)
|
||||
local result = slickmath.cross(a, b, c)
|
||||
return slickmath.sign(result, E)
|
||||
end
|
||||
|
||||
--- Checks if `d` is inside the circumscribed circle created by `a`, `b`, and `c`
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param c slick.geometry.point
|
||||
--- @param d slick.geometry.point
|
||||
--- @return -1 | 0 | 1
|
||||
function slickmath.inside(a, b, c, d)
|
||||
local ax = a.x - d.x
|
||||
local ay = a.y - d.y
|
||||
local bx = b.x - d.x
|
||||
local by = b.y - d.y
|
||||
local cx = c.x - d.x
|
||||
local cy = c.y - d.y
|
||||
|
||||
local i = (ax * ax + ay * ay) * (bx * cy - cx * by)
|
||||
local j = (bx * bx + by * by) * (ax * cy - cx * ay)
|
||||
local k = (cx * cx + cy * cy) * (ax * by - bx * ay)
|
||||
local result = i - j + k
|
||||
|
||||
return slickmath.sign(result)
|
||||
end
|
||||
|
||||
local function _collinear(a, b, c, d, E)
|
||||
local abl = math.min(a, b)
|
||||
local abh = math.max(a, b)
|
||||
|
||||
local cdl = math.min(c, d)
|
||||
local cdh = math.max(c, d)
|
||||
|
||||
if cdh + E < abl or abh + E < cdl then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param c slick.geometry.point
|
||||
--- @param d slick.geometry.point
|
||||
--- @return boolean
|
||||
function slickmath.collinear(a, b, c, d, E)
|
||||
E = E or 0
|
||||
|
||||
local acdSign = slickmath.direction(a, c, d, E)
|
||||
local bcdSign = slickmath.direction(b, c, d, E)
|
||||
local cabSign = slickmath.direction(c, a, b, E)
|
||||
local dabSign = slickmath.direction(d, a, b, E)
|
||||
|
||||
if acdSign == 0 and bcdSign == 0 and cabSign == 0 and dabSign == 0 then
|
||||
return _collinear(a.x, b.x, c.x, d.x, E) and _collinear(a.y, b.y, c.y, d.y, E)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param c slick.geometry.point
|
||||
--- @param d slick.geometry.point
|
||||
--- @param E number?
|
||||
--- @return boolean, number?, number?, number?, number?
|
||||
function slickmath.intersection(a, b, c, d, E)
|
||||
E = E or 0
|
||||
|
||||
local acdSign = slickmath.direction(a, c, d, E)
|
||||
local bcdSign = slickmath.direction(b, c, d, E)
|
||||
if (acdSign < 0 and bcdSign < 0) or (acdSign > 0 and bcdSign > 0) then
|
||||
return false
|
||||
end
|
||||
|
||||
local cabSign = slickmath.direction(c, a, b, E)
|
||||
local dabSign = slickmath.direction(d, a, b, E)
|
||||
if (cabSign < 0 and dabSign < 0) or (cabSign > 0 and dabSign > 0) then
|
||||
return false
|
||||
end
|
||||
|
||||
if acdSign == 0 and bcdSign == 0 and cabSign == 0 and dabSign == 0 then
|
||||
return slickmath.collinear(a, b, c, d, E)
|
||||
end
|
||||
|
||||
local bax = b.x - a.x
|
||||
local bay = b.y - a.y
|
||||
local dcx = d.x - c.x
|
||||
local dcy = d.y - c.y
|
||||
|
||||
local baCrossDC = bax * dcy - bay * dcx
|
||||
local dcCrossBA = dcx * bay - dcy * bax
|
||||
if baCrossDC == 0 or dcCrossBA == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local acx = a.x - c.x
|
||||
local acy = a.y - c.y
|
||||
local cax = c.x - a.x
|
||||
local cay = c.y - a.y
|
||||
|
||||
local dcCrossAC = dcx * acy - dcy * acx
|
||||
local baCrossCA = bax * cay - bay * cax
|
||||
|
||||
local u = dcCrossAC / baCrossDC
|
||||
local v = baCrossCA / dcCrossBA
|
||||
|
||||
if u < -E or u > (1 + E) or v < -E or v > (1 + E) then
|
||||
return false
|
||||
end
|
||||
|
||||
local rx = a.x + bax * u
|
||||
local ry = a.y + bay * u
|
||||
|
||||
return true, rx, ry, u, v
|
||||
end
|
||||
|
||||
--- @param s slick.geometry.segment
|
||||
--- @param p slick.geometry.point
|
||||
--- @param r number
|
||||
--- @param E number?
|
||||
--- @return boolean, number?, number?
|
||||
function slickmath.lineCircleIntersection(s, p, r, E)
|
||||
E = E or 0
|
||||
|
||||
local p1 = s.a
|
||||
local p2 = s.b
|
||||
|
||||
local rSquared = r ^ 2
|
||||
|
||||
local dx = p2.x - p1.x
|
||||
local dy = p2.y - p1.y
|
||||
|
||||
local fx = p1.x - p.x
|
||||
local fy = p1.y - p.y
|
||||
|
||||
local a = dx ^ 2 + dy ^ 2
|
||||
local b = 2 * (dx * fx + dy * fy)
|
||||
local c = fx ^ 2 + fy ^ 2 - rSquared
|
||||
|
||||
local d = b ^ 2 - 4 * a * c
|
||||
if a <= 0 or d < -E then
|
||||
return false, nil, nil
|
||||
end
|
||||
|
||||
d = math.sqrt(math.max(d, 0))
|
||||
|
||||
local u = (-b - d) / (2 * a)
|
||||
local v = (-b + d) / (2 * a)
|
||||
|
||||
return true, u, v
|
||||
end
|
||||
|
||||
--- @param p1 slick.geometry.point
|
||||
--- @param r1 number
|
||||
--- @param p2 slick.geometry.point
|
||||
--- @param r2 number
|
||||
--- @return boolean, number?, number?, number?, number?
|
||||
function slickmath.circleCircleIntersection(p1, r1, p2, r2)
|
||||
local nx = p2.x - p1.x
|
||||
local ny = p2.y - p1.y
|
||||
|
||||
local radius = r1 + r2
|
||||
local magnitude = nx ^ 2 + ny ^ 2
|
||||
if magnitude <= radius ^ 2 then
|
||||
if magnitude == 0 then
|
||||
return true, nil, nil, nil, nil
|
||||
elseif magnitude < math.abs(r1 - r2) ^ 2 then
|
||||
return true, nil, nil, nil, nil
|
||||
end
|
||||
|
||||
local d = math.sqrt(magnitude)
|
||||
|
||||
if d > 0 then
|
||||
nx = nx / d
|
||||
ny = ny / d
|
||||
end
|
||||
|
||||
local a = (r1 ^ 2 - r2 ^ 2 + magnitude) / (2 * d)
|
||||
local h = math.sqrt(r1 ^ 2 - a ^ 2)
|
||||
|
||||
local directionX = p2.x - p1.x
|
||||
local directionY = p2.y - p1.y
|
||||
local p3x = p1.x + a * directionX / d
|
||||
local p3y = p1.y + a * directionY / d
|
||||
|
||||
local result1X = p3x + h * directionY / d
|
||||
local result1Y = p3y - h * directionX / d
|
||||
|
||||
local result2X = p3x - h * directionY / d
|
||||
local result2Y = p3y + h * directionX / d
|
||||
|
||||
return true, result1X, result1Y, result2X, result2Y
|
||||
end
|
||||
|
||||
return false, nil, nil, nil, nil
|
||||
end
|
||||
|
||||
--- @param value number
|
||||
--- @param E number?
|
||||
--- @return -1 | 0 | 1
|
||||
function slickmath.sign(value, E)
|
||||
E = E or 0
|
||||
|
||||
if math.abs(value) <= E then
|
||||
return 0
|
||||
end
|
||||
|
||||
if value > 0 then
|
||||
return 1
|
||||
elseif value < 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
--- @param min number
|
||||
--- @param max number
|
||||
--- @param rng love.RandomGenerator?
|
||||
--- @return number
|
||||
function slickmath.random(min, max, rng)
|
||||
if rng then
|
||||
return rng:random(min, max)
|
||||
end
|
||||
|
||||
if love and love.math then
|
||||
return love.math.random(min, max)
|
||||
end
|
||||
|
||||
return math.random(min, max)
|
||||
end
|
||||
|
||||
function slickmath.withinRange(value, min, max, E)
|
||||
E = E or slickmath.EPSILON
|
||||
|
||||
return value > min - E and value < max + E
|
||||
end
|
||||
|
||||
function slickmath.equal(a, b, E)
|
||||
E = E or slickmath.EPSILON
|
||||
|
||||
return math.abs(a - b) < E
|
||||
end
|
||||
|
||||
function slickmath.less(a, b, E)
|
||||
E = E or slickmath.EPSILON
|
||||
|
||||
return a < b + E
|
||||
end
|
||||
|
||||
function slickmath.greater(a, b, E)
|
||||
E = E or slickmath.EPSILON
|
||||
|
||||
return a > b - E
|
||||
end
|
||||
|
||||
function slickmath.lessThanEqual(a, b, E)
|
||||
return slickmath.less(a, b, E) or slickmath.equal(a, b, E)
|
||||
end
|
||||
|
||||
function slickmath.greaterThanEqual(a, b, E)
|
||||
return slickmath.greater(a, b, E) or slickmath.equal(a, b, E)
|
||||
end
|
||||
|
||||
return slickmath
|
||||
39
game/love_src/lib/slick/util/slicktable.lua
Normal file
39
game/love_src/lib/slick/util/slicktable.lua
Normal file
@ -0,0 +1,39 @@
|
||||
local slicktable = {}
|
||||
|
||||
--- @type fun(t: table)
|
||||
local clear
|
||||
do
|
||||
local s, r = pcall(require, "table.clear")
|
||||
if s then
|
||||
clear = r
|
||||
else
|
||||
function clear(t)
|
||||
while #t > 0 do
|
||||
table.remove(t, #t)
|
||||
end
|
||||
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
slicktable.clear = clear
|
||||
|
||||
--- @param t table
|
||||
--- @param i number?
|
||||
--- @param j number?
|
||||
local function reverse(t, i, j)
|
||||
i = i or 1
|
||||
j = j or #t
|
||||
|
||||
if i > j then
|
||||
t[i], t[j] = t[j], t[i]
|
||||
return reverse(t, i + 1, j - 1)
|
||||
end
|
||||
end
|
||||
|
||||
slicktable.reverse = reverse
|
||||
|
||||
return slicktable
|
||||
641
game/love_src/lib/slick/world.lua
Normal file
641
game/love_src/lib/slick/world.lua
Normal file
@ -0,0 +1,641 @@
|
||||
local cache = require("slick.cache")
|
||||
local quadTree = require("slick.collision.quadTree")
|
||||
local entity = require("slick.entity")
|
||||
local point = require("slick.geometry.point")
|
||||
local ray = require("slick.geometry.ray")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local transform = require("slick.geometry.transform")
|
||||
local defaultOptions = require("slick.options")
|
||||
local responses = require("slick.responses")
|
||||
local worldQuery = require("slick.worldQuery")
|
||||
local util = require("slick.util")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @alias slick.worldFilterQueryFunc fun(item: any, other: any, shape: slick.collision.shape, otherShape: slick.collision.shape): string | slick.worldVisitFunc | false
|
||||
local function defaultWorldFilterQueryFunc()
|
||||
return "slide"
|
||||
end
|
||||
|
||||
--- @alias slick.worldShapeFilterQueryFunc fun(item: any, shape: slick.collision.shape): boolean
|
||||
local function defaultWorldShapeFilterQueryFunc()
|
||||
return true
|
||||
end
|
||||
|
||||
--- @alias slick.worldResponseFunc fun(world: slick.world, query: slick.worldQuery, response: slick.worldQueryResponse, x: number, y: number, goalX: number, goalY: number, filter: slick.worldFilterQueryFunc, result: slick.worldQuery): number, number, number, number, string?, slick.worldQueryResponse
|
||||
--- @alias slick.worldVisitFunc fun(item: any, world: slick.world, query: slick.worldQuery, response: slick.worldQueryResponse, x: number, y: number, goalX: number, goalY: number, projection: boolean): string
|
||||
|
||||
--- @class slick.world
|
||||
--- @field cache slick.cache
|
||||
--- @field quadTree slick.collision.quadTree
|
||||
--- @field options slick.options
|
||||
--- @field quadTreeOptions slick.collision.quadTreeOptions
|
||||
--- @field private responses table<string, slick.worldResponseFunc>
|
||||
--- @field private entities slick.entity[]
|
||||
--- @field private itemToEntity table<any, number>
|
||||
--- @field private freeWorldQueries slick.worldQuery[]
|
||||
--- @field private freeList number[]
|
||||
--- @field private cachedQuery slick.worldQuery
|
||||
--- @field private cachedPushQuery slick.worldQuery
|
||||
local world = {}
|
||||
local metatable = { __index = world }
|
||||
|
||||
--- @param t slick.collision.quadTreeOptions?
|
||||
--- @param width number?
|
||||
--- @param height number?
|
||||
--- @param options slick.options?
|
||||
--- @return slick.collision.quadTreeOptions
|
||||
local function _getQuadTreeOptions(t, width, height, options)
|
||||
t = t or {}
|
||||
options = options or defaultOptions
|
||||
|
||||
t.width = width or t.width
|
||||
t.height = height or t.height
|
||||
t.x = options.quadTreeX or t.x or defaultOptions.quadTreeX
|
||||
t.y = options.quadTreeY or t.y or defaultOptions.quadTreeY
|
||||
t.maxLevels = options.quadTreeMaxLevels or t.maxLevels or defaultOptions.quadTreeMaxLevels
|
||||
t.maxData = options.quadTreeMaxData or t.maxData or defaultOptions.quadTreeMaxData
|
||||
t.expand = options.quadTreeExpand == nil and (t.expand == nil and defaultOptions.quadTreeExpand or t.expand) or options.quadTreeExpand
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
--- @param width number
|
||||
--- @param height number
|
||||
--- @param options slick.options?
|
||||
function world.new(width, height, options)
|
||||
assert(type(width) == "number" and width > 0, "expected width to be number > 0")
|
||||
assert(type(height) == "number" and height > 0, "expected height to be number > 0")
|
||||
|
||||
options = options or defaultOptions
|
||||
|
||||
local quadTreeOptions = _getQuadTreeOptions({}, width, height, options)
|
||||
|
||||
local selfOptions = {
|
||||
debug = options.debug == nil and defaultOptions.debug or options.debug,
|
||||
epsilon = options.epsilon or defaultOptions.epsilon or slickmath.EPSILON,
|
||||
maxBounces = options.maxBounces or defaultOptions.maxBounces,
|
||||
maxJitter = options.maxJitter or defaultOptions.maxJitter,
|
||||
quadTreeOptimizationMargin = options.quadTreeOptimizationMargin or defaultOptions.quadTreeOptimizationMargin
|
||||
}
|
||||
|
||||
local self = setmetatable({
|
||||
cache = cache.new(options),
|
||||
options = selfOptions,
|
||||
quadTreeOptions = quadTreeOptions,
|
||||
quadTree = quadTree.new(quadTreeOptions),
|
||||
entities = {},
|
||||
itemToEntity = {},
|
||||
freeList = {},
|
||||
visited = {},
|
||||
responses = {},
|
||||
freeWorldQueries = {}
|
||||
}, metatable)
|
||||
|
||||
self.cachedQuery = worldQuery.new(self)
|
||||
self.cachedPushQuery = worldQuery.new(self)
|
||||
|
||||
self:addResponse("slide", responses.slide)
|
||||
self:addResponse("touch", responses.touch)
|
||||
self:addResponse("cross", responses.cross)
|
||||
self:addResponse("bounce", responses.bounce)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local _cachedTransform = transform.new()
|
||||
|
||||
--- @overload fun(e: slick.entity, x: number, y: number, shape: slick.collision.shapelike): slick.entity
|
||||
--- @overload fun(e: slick.entity, transform: slick.geometry.transform, shape: slick.collision.shapelike): slick.entity
|
||||
--- @return slick.geometry.transform, slick.collision.shapeDefinition
|
||||
local function _getTransformShapes(e, a, b, c)
|
||||
if type(a) == "number" and type(b) == "number" then
|
||||
e.transform:copy(_cachedTransform)
|
||||
_cachedTransform:setTransform(a, b)
|
||||
|
||||
--- @cast c slick.collision.shapeDefinition
|
||||
return _cachedTransform, c
|
||||
end
|
||||
|
||||
assert(util.is(a, transform))
|
||||
|
||||
--- @cast a slick.geometry.transform
|
||||
--- @cast b slick.collision.shapeDefinition
|
||||
return a, b
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
--- @return slick.entity
|
||||
--- @overload fun(self: slick.world, item: any, x: number, y: number, shape: slick.collision.shapeDefinition): slick.entity
|
||||
--- @overload fun(self: slick.world, item: any, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition): slick.entity
|
||||
function world:add(item, a, b, c)
|
||||
assert(not self:has(item), "item exists in world")
|
||||
|
||||
--- @type slick.entity
|
||||
local e
|
||||
|
||||
--- @type number
|
||||
local i
|
||||
if #self.freeList > 0 then
|
||||
i = table.remove(self.freeList)
|
||||
e = self.entities[i]
|
||||
else
|
||||
e = entity.new()
|
||||
table.insert(self.entities, e)
|
||||
i = #self.entities
|
||||
end
|
||||
|
||||
e:init(item)
|
||||
|
||||
local transform, shapes = _getTransformShapes(e, a, b, c)
|
||||
e:setTransform(transform)
|
||||
e:setShapes(shapes)
|
||||
e:add(self)
|
||||
|
||||
self.itemToEntity[item] = i
|
||||
|
||||
--- @type slick.worldQuery
|
||||
local query = table.remove(self.freeWorldQueries) or worldQuery.new(self)
|
||||
query:reset()
|
||||
|
||||
return e
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
--- @return slick.entity
|
||||
function world:get(item)
|
||||
return self.entities[self.itemToEntity[item]]
|
||||
end
|
||||
|
||||
--- @param items any[]?
|
||||
--- @return any[]
|
||||
function world:getItems(items)
|
||||
items = items or {}
|
||||
slicktable.clear(items)
|
||||
|
||||
for item in pairs(self.itemToEntity) do
|
||||
table.insert(items, item)
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
function world:has(item)
|
||||
return self:get(item) ~= nil
|
||||
end
|
||||
|
||||
--- @overload fun(self: slick.world, item: any, x: number, y: number, shape: slick.collision.shapeDefinition): number, number
|
||||
--- @overload fun(self: slick.world, item: any, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition): number, number
|
||||
function world:update(item, a, b, c)
|
||||
local e = self:get(item)
|
||||
|
||||
local transform, shapes = _getTransformShapes(e, a, b, c)
|
||||
if shapes then
|
||||
e:setShapes(shapes)
|
||||
end
|
||||
e:setTransform(transform)
|
||||
|
||||
return transform.x, transform.y
|
||||
end
|
||||
|
||||
--- @overload fun(self: slick.world, item: any, filter: slick.worldFilterQueryFunc, x: number, y: number, shape: slick.collision.shapeDefinition?): number, number
|
||||
--- @overload fun(self: slick.world, item: any, filter: slick.worldFilterQueryFunc, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition?): number, number
|
||||
function world:push(item, filter, a, b, c)
|
||||
local e = self:get(item)
|
||||
local transform, shapes = _getTransformShapes(e, a, b, c)
|
||||
self:update(item, transform, shapes)
|
||||
|
||||
local cachedQuery = self.cachedQuery
|
||||
local x, y = transform.x, transform.y
|
||||
local originalX, originalY = x, y
|
||||
|
||||
local visited = self.cachedPushQuery
|
||||
visited:reset()
|
||||
|
||||
self:project(item, x, y, x, y, filter, cachedQuery)
|
||||
while #cachedQuery.results > 0 do
|
||||
--- @type slick.worldQueryResponse
|
||||
local result
|
||||
for _, r in ipairs(cachedQuery.results) do
|
||||
if r.offset:lengthSquared() > 0 then
|
||||
result = r
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not result then
|
||||
break
|
||||
end
|
||||
|
||||
local count = 0
|
||||
for _, visitedResult in ipairs(visited.results) do
|
||||
if visitedResult.shape == result.shape and visitedResult.otherShape == result.otherShape then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushFactor = 1.1 ^ count
|
||||
local offsetX, offsetY = result.offset.x, result.offset.y
|
||||
offsetX = offsetX * pushFactor
|
||||
offsetY = offsetY * pushFactor
|
||||
|
||||
x = x + offsetX
|
||||
y = y + offsetY
|
||||
|
||||
visited:push(result)
|
||||
self:project(item, x, y, x, y, filter, cachedQuery)
|
||||
end
|
||||
|
||||
self:project(item, x, y, originalX, originalY, filter, cachedQuery)
|
||||
if #cachedQuery.results >= 1 then
|
||||
local result = cachedQuery.results[1]
|
||||
x, y = result.touch.x, result.touch.y
|
||||
end
|
||||
|
||||
transform:setTransform(x, y)
|
||||
e:setTransform(transform)
|
||||
|
||||
return x, y
|
||||
end
|
||||
|
||||
local _cachedRotateBounds = rectangle.new()
|
||||
local _cachedRotateItems = {}
|
||||
|
||||
--- @param item any
|
||||
--- @param angle number
|
||||
--- @param rotateFilter slick.worldFilterQueryFunc
|
||||
--- @param pushFilter slick.worldFilterQueryFunc
|
||||
function world:rotate(item, angle, rotateFilter, pushFilter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
|
||||
local e = self:get(item)
|
||||
|
||||
e.transform:copy(_cachedTransform)
|
||||
_cachedTransform:setTransform(nil, nil, angle)
|
||||
|
||||
_cachedRotateBounds:init(e.bounds:left(), e.bounds:top(), e.bounds:right(), e.bounds:bottom())
|
||||
e:setTransform(_cachedTransform)
|
||||
_cachedRotateBounds:expand(e.bounds.topLeft.x, e.bounds.topLeft.y)
|
||||
_cachedRotateBounds:expand(e.bounds.bottomRight.x, e.bounds.bottomRight.y)
|
||||
|
||||
slicktable.clear(_cachedRotateItems)
|
||||
_cachedRotateItems[item] = true
|
||||
|
||||
local responses, numResponses = self:queryRectangle(_cachedRotateBounds:left(), _cachedRotateBounds:top(), _cachedRotateBounds:width(), _cachedRotateBounds:height(), rotateFilter, query)
|
||||
for _, response in ipairs(responses) do
|
||||
if not _cachedRotateItems[response.item] then
|
||||
_cachedRotateItems[response.item] = true
|
||||
self:push(response.item, pushFilter, response.entity.transform.x, response.entity.transform.y)
|
||||
end
|
||||
end
|
||||
|
||||
return responses, numResponses, query
|
||||
end
|
||||
|
||||
world.wiggle = world.push
|
||||
|
||||
--- @param deltaTime number
|
||||
function world:frame(deltaTime)
|
||||
-- Nothing for now.
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
function world:remove(item)
|
||||
local entityIndex = self.itemToEntity[item]
|
||||
local e = self.entities[entityIndex]
|
||||
|
||||
e:detach()
|
||||
table.insert(self.freeList, entityIndex)
|
||||
|
||||
self.itemToEntity[item] = nil
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:project(item, x, y, goalX, goalY, filter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
local e = self:get(item)
|
||||
|
||||
query:performProjection(e, x, y, goalX, goalY, filter or defaultWorldFilterQueryFunc)
|
||||
|
||||
return query.results, #query.results, query
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param filter slick.worldFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:test(item, x, y, filter, query)
|
||||
return self:project(item, x, y, x, y, filter, query)
|
||||
end
|
||||
|
||||
local _cachedQueryRectangle = rectangle.new()
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param w number
|
||||
--- @param h number
|
||||
--- @param filter slick.worldShapeFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:queryRectangle(x, y, w, h, filter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
|
||||
_cachedQueryRectangle:init(x, y, x + w, y + h)
|
||||
query:performPrimitive(_cachedQueryRectangle, filter or defaultWorldShapeFilterQueryFunc)
|
||||
|
||||
return query.results, #query.results, query
|
||||
end
|
||||
|
||||
local _cachedQuerySegment = segment.new()
|
||||
|
||||
--- @param x1 number
|
||||
--- @param y1 number
|
||||
--- @param x2 number
|
||||
--- @param y2 number
|
||||
--- @param filter slick.worldShapeFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:querySegment(x1, y1, x2, y2, filter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
|
||||
_cachedQuerySegment.a:init(x1, y1)
|
||||
_cachedQuerySegment.b:init(x2, y2)
|
||||
query:performPrimitive(_cachedQuerySegment, filter or defaultWorldShapeFilterQueryFunc)
|
||||
|
||||
return query.results, #query.results, query
|
||||
end
|
||||
|
||||
local _cachedQueryRay = ray.new()
|
||||
|
||||
--- @param originX number
|
||||
--- @param originY number
|
||||
--- @param directionX number
|
||||
--- @param directionY number
|
||||
--- @param filter slick.worldShapeFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:queryRay(originX, originY, directionX, directionY, filter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
|
||||
_cachedQueryRay.origin:init(originX, originY)
|
||||
_cachedQueryRay.direction:init(directionX, directionY)
|
||||
if _cachedQueryRay.direction:lengthSquared() > 0 then
|
||||
_cachedQueryRay.direction:normalize(_cachedQueryRay.direction)
|
||||
end
|
||||
|
||||
query:performPrimitive(_cachedQueryRay, filter or defaultWorldShapeFilterQueryFunc)
|
||||
|
||||
return query.results, #query.results, query
|
||||
end
|
||||
|
||||
local _cachedQueryPoint = point.new()
|
||||
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param filter slick.worldShapeFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:queryPoint(x, y, filter, query)
|
||||
query = query or worldQuery.new(self)
|
||||
|
||||
_cachedQueryPoint:init(x, y)
|
||||
query:performPrimitive(_cachedQueryPoint, filter or defaultWorldShapeFilterQueryFunc)
|
||||
|
||||
return query.results, #query.results, query
|
||||
end
|
||||
|
||||
--- @param result slick.worldQueryResponse
|
||||
--- @param query slick.worldQuery
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param projection? boolean
|
||||
--- @return string
|
||||
function world:respond(result, query, x, y, goalX, goalY, projection)
|
||||
--- @type string
|
||||
local responseName
|
||||
if type(result.response) == "function" or type(result.response) == "table" then
|
||||
responseName = result.response(result.item, self, query, result, x, y, goalX, goalY, not not projection)
|
||||
elseif type(result.response) == "string" then
|
||||
--- @diagnostic disable-next-line: cast-local-type
|
||||
responseName = result.response
|
||||
else
|
||||
responseName = "slide"
|
||||
end
|
||||
result.response = responseName
|
||||
|
||||
--- @cast responseName string
|
||||
return responseName
|
||||
end
|
||||
|
||||
local _cachedRemappedHandlers = {}
|
||||
|
||||
--- @param item any
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return number, number, slick.worldQueryResponse[], number, slick.worldQuery
|
||||
function world:check(item, goalX, goalY, filter, query)
|
||||
if query then
|
||||
query:reset()
|
||||
else
|
||||
query = worldQuery.new(self)
|
||||
end
|
||||
|
||||
slicktable.clear(_cachedRemappedHandlers)
|
||||
|
||||
|
||||
local cachedQuery = self.cachedQuery
|
||||
filter = filter or defaultWorldFilterQueryFunc
|
||||
|
||||
local e = self:get(item)
|
||||
local x, y = e.transform.x, e.transform.y
|
||||
|
||||
self:project(item, x, y, goalX, goalY, filter, cachedQuery)
|
||||
if #cachedQuery.results == 0 then
|
||||
return goalX, goalY, query.results, #query.results, query
|
||||
end
|
||||
|
||||
local previousX, previousY
|
||||
|
||||
local actualX, actualY
|
||||
local bounces = 0
|
||||
while bounces < self.options.maxBounces and #cachedQuery.results > 0 do
|
||||
bounces = bounces + 1
|
||||
|
||||
local result = cachedQuery.results[1]
|
||||
|
||||
--- @type slick.collision.shape
|
||||
local shape, otherShape
|
||||
repeat
|
||||
shape = result.shape
|
||||
otherShape = result.otherShape
|
||||
|
||||
--- @type string
|
||||
local responseName = self:respond(result, query, x, y, goalX, goalY, false)
|
||||
responseName = _cachedRemappedHandlers[otherShape] or responseName
|
||||
|
||||
assert(type(responseName) == "string", "expect name of response handler as string")
|
||||
|
||||
local response = self:getResponse(responseName)
|
||||
|
||||
local remappedResponseName, nextResult
|
||||
x, y, goalX, goalY, remappedResponseName, nextResult = response(self, cachedQuery, result, x, y, goalX, goalY, filter, query)
|
||||
|
||||
--- @cast otherShape slick.collision.shapelike
|
||||
_cachedRemappedHandlers[otherShape] = remappedResponseName
|
||||
|
||||
result = nextResult
|
||||
until not result or (shape == result.shape and otherShape == result.otherShape)
|
||||
|
||||
local isStationary = x == goalX and y == goalY
|
||||
local didMove = not (x == previousX and y == previousY)
|
||||
|
||||
local isSameCollision = #cachedQuery.results >= 1 and cachedQuery.results[1].shape == shape and cachedQuery.results[1].otherShape == otherShape
|
||||
for i = 2, #cachedQuery.results do
|
||||
if isSameCollision then
|
||||
break
|
||||
end
|
||||
|
||||
if cachedQuery.results[i].time > cachedQuery.results[1].time then
|
||||
break
|
||||
end
|
||||
|
||||
if cachedQuery.results[i].shape == shape and cachedQuery.results[i].otherShape == otherShape then
|
||||
isSameCollision = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local hasNoCollisions = #cachedQuery.results == 0
|
||||
|
||||
if hasNoCollisions or isStationary then
|
||||
actualX = goalX
|
||||
actualY = goalY
|
||||
break
|
||||
else
|
||||
actualX = x
|
||||
actualY = y
|
||||
end
|
||||
|
||||
if didMove and not result then
|
||||
break
|
||||
end
|
||||
|
||||
if not didMove and isSameCollision then
|
||||
break
|
||||
end
|
||||
|
||||
previousX, previousY = x, y
|
||||
end
|
||||
|
||||
return actualX, actualY, query.results, #query.results, query
|
||||
end
|
||||
|
||||
--- @param item any
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc?
|
||||
--- @param query slick.worldQuery?
|
||||
--- @return number
|
||||
--- @return number
|
||||
--- @return slick.worldQueryResponse[]
|
||||
--- @return number
|
||||
--- @return slick.worldQuery
|
||||
function world:move(item, goalX, goalY, filter, query)
|
||||
local actualX, actualY, _, _, query = self:check(item, goalX, goalY, filter, query)
|
||||
self:update(item, actualX, actualY)
|
||||
|
||||
return actualX, actualY, query.results, #query.results, query
|
||||
end
|
||||
|
||||
--- @param width number?
|
||||
--- @param height number?
|
||||
--- @param options slick.options?
|
||||
function world:optimize(width, height, options)
|
||||
local x1, y1, x2, y2 = self.quadTree:computeExactBounds()
|
||||
|
||||
local realWidth = x2 - x1
|
||||
local realHeight = y2 - y1
|
||||
|
||||
width = width or realWidth
|
||||
height = height or realHeight
|
||||
|
||||
local x = options and options.quadTreeX or x1
|
||||
local y = options and options.quadTreeY or y1
|
||||
|
||||
local margin = options and options.quadTreeOptimizationMargin or self.options.quadTreeOptimizationMargin
|
||||
self.options.quadTreeOptimizationMargin = margin
|
||||
|
||||
x = x - realWidth * (margin / 2)
|
||||
y = y - realHeight * (margin / 2)
|
||||
width = width * (1 + margin / 2)
|
||||
height = height * (1 + margin / 2)
|
||||
|
||||
self.quadTreeOptions.x = x
|
||||
self.quadTreeOptions.y = y
|
||||
self.quadTreeOptions.width = width
|
||||
self.quadTreeOptions.height = height
|
||||
|
||||
_getQuadTreeOptions(self.quadTreeOptions, width, height, options)
|
||||
self.quadTree:rebuild(self.quadTreeOptions)
|
||||
end
|
||||
|
||||
--- @package
|
||||
--- @param shape slick.collision.shape
|
||||
function world:_addShape(shape)
|
||||
self.quadTree:update(shape, shape.bounds)
|
||||
end
|
||||
|
||||
--- @package
|
||||
--- @param shape slick.collision.shape
|
||||
function world:_removeShape(shape)
|
||||
if self.quadTree:has(shape) then
|
||||
self.quadTree:remove(shape)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
--- @param response slick.worldResponseFunc
|
||||
function world:addResponse(name, response)
|
||||
assert(not self.responses[name])
|
||||
|
||||
self.responses[name] = response
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
function world:removeResponse(name)
|
||||
assert(self.responses[name])
|
||||
|
||||
self.responses[name] = nil
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
--- @return slick.worldResponseFunc
|
||||
function world:getResponse(name)
|
||||
if not self.responses[name] then
|
||||
error(string.format("Unknown collision type: %s", name))
|
||||
end
|
||||
|
||||
return self.responses[name]
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
--- @return boolean
|
||||
function world:hasResponse(name)
|
||||
return self.responses[name] ~= nil
|
||||
end
|
||||
|
||||
return world
|
||||
482
game/love_src/lib/slick/worldQuery.lua
Normal file
482
game/love_src/lib/slick/worldQuery.lua
Normal file
@ -0,0 +1,482 @@
|
||||
local worldQueryResponse = require("slick.worldQueryResponse")
|
||||
local box = require("slick.collision.box")
|
||||
local commonShape = require("slick.collision.commonShape")
|
||||
local quadTreeQuery = require("slick.collision.quadTreeQuery")
|
||||
local ray = require("slick.geometry.ray")
|
||||
local shapeCollisionResolutionQuery = require("slick.collision.shapeCollisionResolutionQuery")
|
||||
local point = require("slick.geometry.point")
|
||||
local rectangle = require("slick.geometry.rectangle")
|
||||
local segment = require("slick.geometry.segment")
|
||||
local transform = require("slick.geometry.transform")
|
||||
local util = require("slick.util")
|
||||
local pool = require ("slick.util.pool")
|
||||
local slickmath = require("slick.util.slickmath")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.worldQuery
|
||||
--- @field world slick.world
|
||||
--- @field quadTreeQuery slick.collision.quadTreeQuery
|
||||
--- @field results slick.worldQueryResponse[]
|
||||
--- @field private cachedResults slick.worldQueryResponse[]
|
||||
--- @field private collisionQuery slick.collision.shapeCollisionResolutionQuery
|
||||
--- @field pools table<any, slick.util.pool> **internal**
|
||||
local worldQuery = {}
|
||||
local metatable = { __index = worldQuery }
|
||||
|
||||
--- @param world slick.world
|
||||
--- @return slick.worldQuery
|
||||
function worldQuery.new(world)
|
||||
return setmetatable({
|
||||
world = world,
|
||||
quadTreeQuery = quadTreeQuery.new(world.quadTree),
|
||||
results = {},
|
||||
cachedResults = {},
|
||||
collisionQuery = shapeCollisionResolutionQuery.new(world.options.epsilon),
|
||||
pools = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param type any
|
||||
--- @param ... unknown
|
||||
--- @return any
|
||||
function worldQuery:allocate(type, ...)
|
||||
local p = self:getPool(type)
|
||||
return p:allocate(...)
|
||||
end
|
||||
|
||||
--- @param type any
|
||||
--- @return slick.util.pool
|
||||
function worldQuery:getPool(type)
|
||||
local p = self.pools[type]
|
||||
if not p then
|
||||
p = pool.new(type)
|
||||
self.pools[type] = p
|
||||
end
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
local _cachedQueryTransform = transform.new()
|
||||
local _cachedQueryBoxShape = box.new(nil, 0, 0, 1, 1)
|
||||
local _cachedQueryVelocity = point.new()
|
||||
local _cachedQueryOffset = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param shape slick.collision.shapeInterface
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:_performShapeQuery(shape, filter)
|
||||
for _, otherShape in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast otherShape slick.collision.shapeInterface
|
||||
local response = filter(otherShape.entity.item, otherShape)
|
||||
|
||||
if response then
|
||||
self.collisionQuery:performProjection(shape, otherShape, _cachedQueryOffset, _cachedQueryOffset, _cachedQueryVelocity, _cachedQueryVelocity)
|
||||
if self.collisionQuery.collision then
|
||||
self:_addCollision(otherShape, nil, response, shape.center, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param p slick.geometry.point
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:_performPrimitivePointQuery(p, filter)
|
||||
self.collisionQuery:reset()
|
||||
|
||||
for _, otherShape in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast otherShape slick.collision.shapeInterface
|
||||
local response = filter(otherShape.entity.item, otherShape)
|
||||
if response then
|
||||
local inside = otherShape:inside(p)
|
||||
if inside then
|
||||
self:_addCollision(otherShape, nil, response, _cachedQueryOffset, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedRayQueryTouch = point.new()
|
||||
local _cachedRayNormal = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param r slick.geometry.ray
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:_performPrimitiveRayQuery(r, filter)
|
||||
self.collisionQuery:reset()
|
||||
|
||||
for _, otherShape in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast otherShape slick.collision.shapeInterface
|
||||
local response = filter(otherShape.entity.item, otherShape)
|
||||
if response then
|
||||
local inside, x, y = otherShape:raycast(r, _cachedRayNormal)
|
||||
if inside and x and y then
|
||||
_cachedRayQueryTouch:init(x, y)
|
||||
|
||||
local result = self:_addCollision(otherShape, nil, response, _cachedRayQueryTouch, true)
|
||||
result.contactPoint:init(x, y)
|
||||
result.distance = _cachedRayQueryTouch:distance(r.origin)
|
||||
|
||||
if otherShape.vertexCount == 2 then
|
||||
self:_correctLineSegmentNormals(r.origin, otherShape.vertices[1], otherShape.vertices[2], result.normal)
|
||||
else
|
||||
result.normal:init(_cachedRayNormal.x, _cachedRayNormal.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param r slick.geometry.rectangle
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:_performPrimitiveRectangleQuery(r, filter)
|
||||
_cachedQueryTransform:setTransform(r:left(), r:top(), 0, r:width(), r:height())
|
||||
_cachedQueryBoxShape:transform(_cachedQueryTransform)
|
||||
|
||||
self:_performShapeQuery(_cachedQueryBoxShape, filter)
|
||||
end
|
||||
|
||||
local _lineSegmentRelativePosition = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param point slick.geometry.point
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param normal slick.geometry.point
|
||||
function worldQuery:_correctLineSegmentNormals(point, a, b, normal)
|
||||
a:direction(b, normal)
|
||||
normal:normalize(normal)
|
||||
|
||||
local side = slickmath.direction(a, b, point, self.world.options.epsilon)
|
||||
if side == 0 then
|
||||
point:sub(a, _lineSegmentRelativePosition)
|
||||
local dotA = normal:dot(_lineSegmentRelativePosition)
|
||||
|
||||
point:sub(b, _lineSegmentRelativePosition)
|
||||
local dotB = normal:dot(_lineSegmentRelativePosition)
|
||||
|
||||
if dotA < 0 and dotB < 0 then
|
||||
normal:negate(normal)
|
||||
end
|
||||
else
|
||||
normal:left(normal)
|
||||
normal:multiplyScalar(side, normal)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param segment slick.geometry.segment
|
||||
--- @param result slick.worldQueryResponse
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
function worldQuery:_addLineSegmentContactPoint(segment, result, x, y)
|
||||
for _, c in ipairs(result.contactPoints) do
|
||||
if c.x == x and c.y == y then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local contactPoint = self:allocate(point, x, y)
|
||||
table.insert(result.contactPoints, contactPoint)
|
||||
|
||||
local distance = contactPoint:distance(segment.a)
|
||||
if distance < result.distance then
|
||||
result.distance = distance
|
||||
result.contactPoint:init(x, y)
|
||||
result.touch:init(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedLineSegmentIntersectionPoint = point.new()
|
||||
|
||||
--- @private
|
||||
--- @param otherShape slick.collision.commonShape
|
||||
--- @param segment slick.geometry.segment
|
||||
--- @param a slick.geometry.point
|
||||
--- @param b slick.geometry.point
|
||||
--- @param response boolean
|
||||
--- @param result slick.worldQueryResponse
|
||||
function worldQuery:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result)
|
||||
local intersection, x, y, u, v = slickmath.intersection(segment.a, segment.b, a, b, self.world.options.epsilon)
|
||||
if not intersection or not (u >= 0 and u <= 1 and v >= 0 and v <= 1) then
|
||||
return false, result
|
||||
end
|
||||
|
||||
if not result then
|
||||
_cachedLineSegmentIntersectionPoint:init(x or 0, y or 0)
|
||||
result = self:_addCollision(otherShape, nil, response, _cachedLineSegmentIntersectionPoint, true)
|
||||
result.distance = math.huge
|
||||
end
|
||||
|
||||
if x and y then
|
||||
self:_addLineSegmentContactPoint(segment, result, x, y)
|
||||
else
|
||||
intersection = slickmath.intersection(segment.a, segment.a, a, b, self.world.options.epsilon)
|
||||
if intersection then
|
||||
self:_addLineSegmentContactPoint(segment, result, segment.a.x, segment.a.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(segment.b, segment.b, a, b, self.world.options.epsilon)
|
||||
if intersection then
|
||||
self:_addLineSegmentContactPoint(segment, result, segment.b.x, segment.b.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(a, a, segment.a, segment.b, self.world.options.epsilon)
|
||||
if intersection then
|
||||
self:_addLineSegmentContactPoint(segment, result, a.x, a.y)
|
||||
end
|
||||
|
||||
intersection = slickmath.intersection(b, b, segment.a, segment.b, self.world.options.epsilon)
|
||||
if intersection then
|
||||
self:_addLineSegmentContactPoint(segment, result, b.x, b.y)
|
||||
end
|
||||
end
|
||||
|
||||
return true, result
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param segment slick.geometry.segment
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:_performPrimitiveSegmentQuery(segment, filter)
|
||||
self.collisionQuery:reset()
|
||||
|
||||
for _, otherShape in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast otherShape slick.collision.shapeInterface
|
||||
local response = filter(otherShape.entity.item, otherShape)
|
||||
if response then
|
||||
--- @type slick.worldQueryResponse
|
||||
local result
|
||||
local intersection = false
|
||||
if otherShape.vertexCount == 2 then
|
||||
local a = otherShape.vertices[1]
|
||||
local b = otherShape.vertices[2]
|
||||
|
||||
intersection, result = self:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result)
|
||||
if intersection then
|
||||
self:_correctLineSegmentNormals(segment.a, a, b, result.normal)
|
||||
end
|
||||
else
|
||||
for i = 1, otherShape.vertexCount do
|
||||
local a = otherShape.vertices[i]
|
||||
local b = otherShape.vertices[slickmath.wrap(i, 1, otherShape.vertexCount)]
|
||||
|
||||
local distance = result and result.distance or math.huge
|
||||
intersection, result = self:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result)
|
||||
if intersection then
|
||||
if result.distance < distance then
|
||||
a:direction(b, result.normal)
|
||||
result.normal:normalize(result.normal)
|
||||
result.normal:left(result.normal)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray | slick.collision.commonShape
|
||||
--- @param filter slick.worldShapeFilterQueryFunc
|
||||
function worldQuery:performPrimitive(shape, filter)
|
||||
if util.is(shape, commonShape) then
|
||||
--- @cast shape slick.collision.commonShape
|
||||
self:_beginPrimitiveQuery(shape.bounds)
|
||||
else
|
||||
--- @cast shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray
|
||||
self:_beginPrimitiveQuery(shape)
|
||||
end
|
||||
|
||||
if util.is(shape, rectangle) then
|
||||
--- @cast shape slick.geometry.rectangle
|
||||
self:_performPrimitiveRectangleQuery(shape, filter)
|
||||
elseif util.is(shape, point) then
|
||||
--- @cast shape slick.geometry.point
|
||||
self:_performPrimitivePointQuery(shape, filter)
|
||||
elseif util.is(shape, segment) then
|
||||
--- @cast shape slick.geometry.segment
|
||||
self:_performPrimitiveSegmentQuery(shape, filter)
|
||||
elseif util.is(shape, ray) then
|
||||
--- @cast shape slick.geometry.ray
|
||||
self:_performPrimitiveRayQuery(shape, filter)
|
||||
elseif util.is(shape, commonShape) then
|
||||
--- @cast shape slick.collision.commonShape
|
||||
self:_performShapeQuery(shape, filter)
|
||||
end
|
||||
|
||||
self:_endQuery()
|
||||
end
|
||||
|
||||
local _cachedSelfVelocity = point.new()
|
||||
local _cachedSelfOffset = point.new()
|
||||
local _cachedOtherVelocity = point.new()
|
||||
local _cachedEntityBounds = rectangle.new()
|
||||
local _cachedShapeBounds = rectangle.new()
|
||||
local _cachedSelfPosition = point.new()
|
||||
local _cachedSelfProjectedPosition = point.new()
|
||||
local _cachedSelfOffsetPosition = point.new()
|
||||
local _cachedOtherOffset = point.new()
|
||||
|
||||
--- @param entity slick.entity
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
--- @param filter slick.worldFilterQueryFunc
|
||||
function worldQuery:performProjection(entity, x, y, goalX, goalY, filter)
|
||||
self:_beginQuery(entity, x, y, goalX, goalY)
|
||||
|
||||
_cachedSelfPosition:init(entity.transform.x, entity.transform.y)
|
||||
_cachedSelfProjectedPosition:init(x, y)
|
||||
_cachedSelfPosition:direction(_cachedSelfProjectedPosition, _cachedSelfOffset)
|
||||
|
||||
local offsetX = -entity.transform.x + x
|
||||
local offsetY = -entity.transform.y + y
|
||||
|
||||
_cachedSelfOffsetPosition:init(x, y)
|
||||
|
||||
_cachedSelfVelocity:init(goalX, goalY)
|
||||
_cachedSelfOffsetPosition:direction(_cachedSelfVelocity, _cachedSelfVelocity)
|
||||
|
||||
_cachedEntityBounds:init(entity.bounds:left(), entity.bounds:top(), entity.bounds:right(), entity.bounds:bottom())
|
||||
_cachedEntityBounds:move(offsetX, offsetY)
|
||||
_cachedEntityBounds:sweep(goalX, goalY)
|
||||
|
||||
for _, otherShape in ipairs(self.quadTreeQuery.results) do
|
||||
--- @cast otherShape slick.collision.shapeInterface
|
||||
if otherShape.entity ~= entity and _cachedEntityBounds:overlaps(otherShape.bounds) then
|
||||
for _, shape in ipairs(entity.shapes.shapes) do
|
||||
_cachedShapeBounds:init(shape.bounds:left(), shape.bounds:top(), shape.bounds:right(), shape.bounds:bottom())
|
||||
_cachedShapeBounds:move(offsetX, offsetY)
|
||||
_cachedShapeBounds:sweep(goalX + shape.bounds:left() - entity.transform.x, goalY + shape.bounds:top() - entity.transform.y)
|
||||
|
||||
if _cachedShapeBounds:overlaps(otherShape.bounds) then
|
||||
local response = filter(entity.item, otherShape.entity.item, shape, otherShape)
|
||||
if response then
|
||||
self.collisionQuery:performProjection(shape, otherShape, _cachedSelfOffset, _cachedOtherOffset, _cachedSelfVelocity, _cachedOtherVelocity)
|
||||
|
||||
if self.collisionQuery.collision then
|
||||
self:_addCollision(shape, otherShape, response, _cachedSelfProjectedPosition, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:_endQuery()
|
||||
end
|
||||
|
||||
function worldQuery:sort()
|
||||
table.sort(self.results, worldQueryResponse.less)
|
||||
end
|
||||
|
||||
function worldQuery:reset()
|
||||
slicktable.clear(self.results)
|
||||
|
||||
for _, pool in pairs(self.pools) do
|
||||
pool:reset()
|
||||
end
|
||||
end
|
||||
|
||||
local _cachedBounds = rectangle.new()
|
||||
|
||||
--- @private
|
||||
--- @param entity slick.entity
|
||||
--- @param x number
|
||||
--- @param y number
|
||||
--- @param goalX number
|
||||
--- @param goalY number
|
||||
function worldQuery:_beginQuery(entity, x, y, goalX, goalY)
|
||||
self:reset()
|
||||
|
||||
_cachedBounds:init(entity.bounds:left(), entity.bounds:top(), entity.bounds:right(), entity.bounds:bottom())
|
||||
_cachedBounds:move(-entity.transform.x + x, -entity.transform.y + y)
|
||||
_cachedBounds:sweep(goalX, goalY)
|
||||
|
||||
self.quadTreeQuery:perform(_cachedBounds)
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray
|
||||
function worldQuery:_beginPrimitiveQuery(shape)
|
||||
self:reset()
|
||||
self.quadTreeQuery:perform(shape)
|
||||
end
|
||||
|
||||
--- @private
|
||||
function worldQuery:_endQuery()
|
||||
self:sort()
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param shape slick.collision.shapeInterface
|
||||
--- @param otherShape slick.collision.shapeInterface?
|
||||
--- @param response string | slick.worldVisitFunc | boolean
|
||||
--- @param primitive boolean
|
||||
--- @return slick.worldQueryResponse
|
||||
function worldQuery:_addCollision(shape, otherShape, response, offset, primitive)
|
||||
local index = #self.results + 1
|
||||
local result = self.cachedResults[index]
|
||||
if not result then
|
||||
result = worldQueryResponse.new(self)
|
||||
table.insert(self.cachedResults, result)
|
||||
end
|
||||
|
||||
result:init(shape, otherShape, response, offset, self.collisionQuery)
|
||||
table.insert(self.results, result)
|
||||
|
||||
return self.results[#self.results]
|
||||
end
|
||||
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @return number
|
||||
function worldQuery:getResponseIndex(response)
|
||||
for i, otherResponse in ipairs(self.results) do
|
||||
if otherResponse == response then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return #self.results + 1
|
||||
end
|
||||
|
||||
--- @param shape slick.collision.shape
|
||||
--- @param otherShape slick.collision.shape
|
||||
--- @return integer?
|
||||
function worldQuery:getShapeResponseIndex(shape, otherShape)
|
||||
for i, response in ipairs(self.results) do
|
||||
if response.shape == shape and response.otherShape == otherShape then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- @param response slick.worldQueryResponse
|
||||
--- @param copy boolean?
|
||||
function worldQuery:push(response, copy)
|
||||
if copy == nil then
|
||||
copy = true
|
||||
end
|
||||
|
||||
local index = #self.results + 1
|
||||
local result = self.cachedResults[index]
|
||||
if not result then
|
||||
result = worldQueryResponse.new(self)
|
||||
table.insert(self.cachedResults, result)
|
||||
end
|
||||
|
||||
response:move(result, copy)
|
||||
table.insert(self.results, result)
|
||||
end
|
||||
|
||||
--- @param other slick.worldQuery
|
||||
--- @param copy boolean?
|
||||
function worldQuery:move(other, copy)
|
||||
for _, response in ipairs(self.results) do
|
||||
other:push(response, copy)
|
||||
end
|
||||
end
|
||||
|
||||
return worldQuery
|
||||
214
game/love_src/lib/slick/worldQueryResponse.lua
Normal file
214
game/love_src/lib/slick/worldQueryResponse.lua
Normal file
@ -0,0 +1,214 @@
|
||||
local point = require("slick.geometry.point")
|
||||
local util = require("slick.util")
|
||||
local pool = require("slick.util.pool")
|
||||
local slicktable = require("slick.util.slicktable")
|
||||
|
||||
--- @class slick.worldQueryResponse
|
||||
--- @field query slick.worldQuery
|
||||
--- @field response string | slick.worldVisitFunc | true
|
||||
--- @field item any
|
||||
--- @field entity slick.entity | slick.cache
|
||||
--- @field shape slick.collision.shape
|
||||
--- @field other any?
|
||||
--- @field otherEntity slick.entity | slick.cache | nil
|
||||
--- @field otherShape slick.collision.shape?
|
||||
--- @field normal slick.geometry.point
|
||||
--- @field alternateNormal slick.geometry.point
|
||||
--- @field normals slick.geometry.point[]
|
||||
--- @field alternateNormals slick.geometry.point[]
|
||||
--- @field depth number
|
||||
--- @field alternateDepth number
|
||||
--- @field time number
|
||||
--- @field offset slick.geometry.point
|
||||
--- @field touch slick.geometry.point
|
||||
--- @field isProjection boolean
|
||||
--- @field contactPoint slick.geometry.point
|
||||
--- @field contactPoints slick.geometry.point[]
|
||||
--- @field distance number
|
||||
--- @field extra table
|
||||
local worldQueryResponse = {}
|
||||
local metatable = { __index = worldQueryResponse }
|
||||
|
||||
--- @return slick.worldQueryResponse
|
||||
function worldQueryResponse.new(query)
|
||||
return setmetatable({
|
||||
query = query,
|
||||
response = "slide",
|
||||
normal = point.new(),
|
||||
alternateNormal = point.new(),
|
||||
normals = {},
|
||||
alternateNormals = {},
|
||||
depth = 0,
|
||||
alternateDepth = 0,
|
||||
time = 0,
|
||||
offset = point.new(),
|
||||
touch = point.new(),
|
||||
isProjection = false,
|
||||
contactPoint = point.new(),
|
||||
contactPoints = {},
|
||||
extra = {}
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
--- @param a slick.worldQueryResponse
|
||||
--- @param b slick.worldQueryResponse
|
||||
function worldQueryResponse.less(a, b)
|
||||
if a.time == b.time then
|
||||
if a.depth == b.depth then
|
||||
return a.distance < b.distance
|
||||
else
|
||||
return a.depth > b.depth
|
||||
end
|
||||
end
|
||||
|
||||
return a.time < b.time
|
||||
end
|
||||
|
||||
local _cachedInitItemPosition = point.new()
|
||||
|
||||
--- @param shape slick.collision.shapeInterface
|
||||
--- @param otherShape slick.collision.shapeInterface?
|
||||
--- @param response string | slick.worldVisitFunc | true
|
||||
--- @param position slick.geometry.point
|
||||
--- @param query slick.collision.shapeCollisionResolutionQuery
|
||||
function worldQueryResponse:init(shape, otherShape, response, position, query)
|
||||
self.response = response
|
||||
|
||||
self.shape = shape
|
||||
self.entity = shape.entity
|
||||
self.item = shape.entity.item
|
||||
|
||||
self.otherShape = otherShape
|
||||
self.otherEntity = self.otherShape and self.otherShape.entity
|
||||
self.other = self.otherEntity and self.otherEntity.item
|
||||
|
||||
self.normal:init(query.normal.x, query.normal.y)
|
||||
self.alternateNormal:init(query.currentNormal.x, query.currentNormal.y)
|
||||
self.alternateDepth = query.currentDepth
|
||||
self.depth = query.depth
|
||||
self.time = query.time
|
||||
|
||||
self.offset:init(query.currentOffset.x, query.currentOffset.y)
|
||||
position:add(self.offset, self.touch)
|
||||
|
||||
local closestContactPointDistance = math.huge
|
||||
|
||||
--- @type slick.geometry.point
|
||||
local closestContactPoint
|
||||
|
||||
_cachedInitItemPosition:init(self.entity.transform.x, self.entity.transform.y)
|
||||
|
||||
slicktable.clear(self.contactPoints)
|
||||
for i = 1, query.contactPointsCount do
|
||||
local inputContactPoint = query.contactPoints[i]
|
||||
local outputContactPoint = self.query:allocate(point, inputContactPoint.x, inputContactPoint.y)
|
||||
table.insert(self.contactPoints, outputContactPoint)
|
||||
|
||||
local distanceSquared = outputContactPoint:distance(_cachedInitItemPosition)
|
||||
if distanceSquared < closestContactPointDistance then
|
||||
closestContactPointDistance = distanceSquared
|
||||
closestContactPoint = outputContactPoint
|
||||
end
|
||||
end
|
||||
|
||||
slicktable.clear(self.normals)
|
||||
for _, inputNormal in ipairs(query.normals) do
|
||||
local outputNormal = self.query:allocate(point, inputNormal.x, inputNormal.y)
|
||||
table.insert(self.normals, outputNormal)
|
||||
end
|
||||
|
||||
slicktable.clear(self.alternateNormals)
|
||||
for _, inputNormal in ipairs(query.alternateNormals) do
|
||||
local outputNormal = self.query:allocate(point, inputNormal.x, inputNormal.y)
|
||||
table.insert(self.alternateNormals, outputNormal)
|
||||
end
|
||||
|
||||
if closestContactPoint then
|
||||
self.contactPoint:init(closestContactPoint.x, closestContactPoint.y)
|
||||
else
|
||||
self.contactPoint:init(0, 0)
|
||||
end
|
||||
|
||||
self.distance = self.shape:distance(self.touch)
|
||||
|
||||
slicktable.clear(self.extra)
|
||||
end
|
||||
|
||||
function worldQueryResponse:isTouchingWillNotPenetrate()
|
||||
return self.time == 0 and self.depth == 0
|
||||
end
|
||||
|
||||
function worldQueryResponse:isTouchingWillPenetrate()
|
||||
return self.time == 0 and (self.isProjection and self.depth >= 0 or self.depth > 0)
|
||||
end
|
||||
|
||||
function worldQueryResponse:notTouchingWillTouch()
|
||||
return self.time > 0
|
||||
end
|
||||
|
||||
--- @param other slick.worldQueryResponse
|
||||
--- @param copy boolean?
|
||||
function worldQueryResponse:move(other, copy)
|
||||
other.response = self.response
|
||||
|
||||
other.shape = self.shape
|
||||
other.entity = self.entity
|
||||
other.item = self.item
|
||||
|
||||
other.otherShape = self.otherShape
|
||||
other.otherEntity = self.otherEntity
|
||||
other.other = self.other
|
||||
|
||||
other.normal:init(self.normal.x, self.normal.y)
|
||||
other.alternateNormal:init(self.alternateNormal.x, self.alternateNormal.y)
|
||||
other.depth = self.depth
|
||||
other.alternateDepth = self.alternateDepth
|
||||
other.time = self.time
|
||||
other.offset:init(self.offset.x, self.offset.y)
|
||||
other.touch:init(self.touch.x, self.touch.y)
|
||||
other.isProjection = self.isProjection
|
||||
|
||||
other.contactPoint:init(self.contactPoint.x, self.contactPoint.y)
|
||||
other.distance = self.distance
|
||||
|
||||
slicktable.clear(other.contactPoints)
|
||||
for i, inputContactPoint in ipairs(self.contactPoints) do
|
||||
local outputContactPoint = other.query:allocate(point, inputContactPoint.x, inputContactPoint.y)
|
||||
table.insert(other.contactPoints, outputContactPoint)
|
||||
end
|
||||
|
||||
slicktable.clear(other.normals)
|
||||
for i, inputNormal in ipairs(self.normals) do
|
||||
local outputNormal = other.query:allocate(point, inputNormal.x, inputNormal.y)
|
||||
table.insert(other.normals, outputNormal)
|
||||
end
|
||||
|
||||
slicktable.clear(other.alternateNormals)
|
||||
for i, inputNormal in ipairs(self.alternateNormals) do
|
||||
local outputNormal = other.query:allocate(point, inputNormal.x, inputNormal.y)
|
||||
table.insert(other.alternateNormals, outputNormal)
|
||||
end
|
||||
|
||||
if not copy then
|
||||
slicktable.clear(self.contactPoints)
|
||||
slicktable.clear(self.normals)
|
||||
|
||||
other.extra, self.extra = self.extra, other.extra
|
||||
slicktable.clear(self.extra)
|
||||
|
||||
for key, value in pairs(other.extra) do
|
||||
local keyType = util.type(key)
|
||||
local valueType = util.type(value)
|
||||
|
||||
if keyType then
|
||||
pool.swap(self.query:getPool(keyType), other.query:getPool(keyType), key)
|
||||
end
|
||||
|
||||
if valueType then
|
||||
pool.swap(self.query:getPool(valueType), other.query:getPool(valueType), value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return worldQueryResponse
|
||||
@ -1,3 +1,5 @@
|
||||
local racing = require("love_src.src.system.racing_phy")
|
||||
|
||||
local entity = {}
|
||||
|
||||
entity.__index = entity
|
||||
@ -7,10 +9,13 @@ function entity.load(actor, finish)
|
||||
self.data = {
|
||||
pos = {0, 100, 0},
|
||||
color = {255/255, 255/255, 255/255},
|
||||
|
||||
actor = actor,
|
||||
|
||||
current_speed = 0.0,
|
||||
current_accel = 0.0,
|
||||
race = {
|
||||
speed = 0.0,
|
||||
accel = 10.0,
|
||||
},
|
||||
|
||||
finish = finish
|
||||
}
|
||||
@ -20,28 +25,29 @@ end
|
||||
function entity:update(dt)
|
||||
if (self.data.pos[1] > self.data.finish[1]) then
|
||||
self.data.pos[1] = 0
|
||||
self.data.current_speed = 0
|
||||
self.data.current_accel = 0
|
||||
self.data.race.speed = 0
|
||||
self.data.race.accel = 10.0
|
||||
end
|
||||
self:accel(dt)
|
||||
self.data.pos[1] = self.data.pos[1] + self.data.current_speed * dt
|
||||
self.data.race.speed = racing.accelerate(dt, 1, self.data.race.speed, self.data.actor.data.max_speed, self.data.race.accel)
|
||||
-- self:accel(dt)
|
||||
self.data.pos[1] = self.data.pos[1] + self.data.race.speed * dt
|
||||
end
|
||||
|
||||
function entity:accel(dt)
|
||||
if (self.data.current_accel <= self.data.actor.data.accel) then
|
||||
self.data.current_accel = self.data.current_accel + dt
|
||||
end
|
||||
if (self.data.current_speed <= self.data.actor.data.max_speed) then
|
||||
self.data.current_speed = self.data.current_speed + self.data.current_accel * dt
|
||||
end
|
||||
end
|
||||
-- function entity:accel(dt)
|
||||
-- if (self.data.current_accel <= self.data.actor.data.accel) then
|
||||
-- self.data.current_accel = self.data.current_accel + dt
|
||||
-- end
|
||||
-- if (self.data.current_speed <= self.data.actor.data.max_speed) then
|
||||
-- self.data.current_speed = self.data.current_speed + self.data.current_accel * dt
|
||||
-- end
|
||||
-- end
|
||||
|
||||
function entity:draw()
|
||||
love.graphics.push()
|
||||
love.graphics.setColor(self.data.color[1], self.data.color[2], self.data.color[3])
|
||||
love.graphics.points(self.data.pos[1], self.data.pos[2])
|
||||
love.graphics.print(self.data.current_speed, self.data.pos[1], self.data.pos[2])
|
||||
love.graphics.print(string.format("Current Accel : %s", self.data.current_accel), 0, 40)
|
||||
love.graphics.print(self.data.race.speed, self.data.pos[1], self.data.pos[2])
|
||||
-- love.graphics.print(string.format("Current Accel : %s", self.data.current_accel), 0, 40)
|
||||
love.graphics.pop()
|
||||
self.data.actor:draw()
|
||||
end
|
||||
|
||||
@ -2,12 +2,20 @@ local entity = {}
|
||||
|
||||
entity.__index = entity
|
||||
|
||||
function entity.load(name)
|
||||
local self = setmetatable({}, entity)
|
||||
self.data = {
|
||||
_data = {
|
||||
name = "p1",
|
||||
|
||||
max_speed = 100.0,
|
||||
accel = 2.0,
|
||||
}
|
||||
grip = 2.0,
|
||||
brake = 2.0,
|
||||
|
||||
ui = {0, 0}
|
||||
}
|
||||
|
||||
function entity.load(data)
|
||||
local self = setmetatable({}, entity)
|
||||
self.data = data or _data
|
||||
return self
|
||||
end
|
||||
|
||||
@ -16,8 +24,11 @@ end
|
||||
|
||||
function entity:draw()
|
||||
love.graphics.push()
|
||||
love.graphics.print(string.format("Max Speed : %s", self.data.max_speed), 0, 0)
|
||||
love.graphics.print(string.format("Accel : %s", self.data.accel), 0, 20)
|
||||
love.graphics.print(string.format("Name : %s", self.data.name, self.data.ui[1], self.data.ui[2]))
|
||||
love.graphics.print(string.format("Max Speed : %s", self.data.max_speed), self.data.ui[1], self.data.ui[2] + 20)
|
||||
love.graphics.print(string.format("Accel : %s", self.data.accel),self.data.ui[1], self.data.ui[2] + 40)
|
||||
love.graphics.print(string.format("Grip : %s", self.data.grip), self.data.ui[1], self.data.ui[2] + 60)
|
||||
love.graphics.print(string.format("Brake : %s", self.data.brake), self.data.ui[1], self.data.ui[2] + 80)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
|
||||
@ -1,30 +1,80 @@
|
||||
local slick = require("love_src.lib.slick")
|
||||
|
||||
local racer = require("love_src.src.entities.racing.racer")
|
||||
local player = require("love_src.src.entities.shared.actor").load()
|
||||
|
||||
local mode = {}
|
||||
|
||||
local finish = {100, 100, 0}
|
||||
|
||||
local entities = {}
|
||||
local time = 0
|
||||
|
||||
function mode.load(player)
|
||||
entities.racer = racer.load(player, finish)
|
||||
|
||||
local w, h = 800, 600
|
||||
|
||||
function mode:load()
|
||||
self.entities = {}
|
||||
self.level = {}
|
||||
self.world = slick.newWorld(w, h, {
|
||||
quadTreeX = 0,
|
||||
quadTreeY = 0
|
||||
})
|
||||
|
||||
local new_racer = racer.load(player, finish)
|
||||
|
||||
self.world:add(new_racer, 200, 500, slick.newCircleShape(0, 0, 16))
|
||||
|
||||
table.insert(self.entities, new_racer)
|
||||
|
||||
self.world:add(self.level, 0, 0, slick.newShapeGroup(
|
||||
-- Boxes surrounding the map
|
||||
slick.newRectangleShape(0, 0, w, 8), -- top
|
||||
slick.newRectangleShape(0, 0, 8, h), -- left
|
||||
slick.newRectangleShape(w - 8, 0, 8, h), -- right
|
||||
slick.newRectangleShape(0, h - 8, w, 8), -- bottom
|
||||
-- Triangles in corners
|
||||
slick.newPolygonShape({ 8, h - h / 8, w / 4, h - 8, 8, h - 8 }),
|
||||
slick.newPolygonShape({ w - w / 4, h, w - 8, h / 2, w - 8, h }),
|
||||
-- Convex shape
|
||||
slick.newPolygonMeshShape({ w / 2 + w / 4, h / 4, w / 2 + w / 4 + w / 8, h / 4 + h / 8, w / 2 + w / 4, h / 4 + h / 4, w / 2 + w / 4 + w / 16, h / 4 + h / 8 })
|
||||
))
|
||||
end
|
||||
|
||||
function mode.update(dt)
|
||||
entities.racer:update(dt)
|
||||
local function movePlayer(p, dt)
|
||||
local goalX, goalY = p.x + dt * p.velocityX, p.y + dt * p.velocityY
|
||||
p.x, p.y = world:move(p, goalX, goalY)
|
||||
end
|
||||
|
||||
function mode.draw()
|
||||
entities.racer:draw()
|
||||
function mode:update(dt)
|
||||
for _,e in pairs(self.entities) do
|
||||
e:update(dt)
|
||||
|
||||
-- local goalX, goalY = w / 2, -1000
|
||||
|
||||
-- local actualX, actualY, collisions, count = self.world:move(e, goalX, goalY, function(item, other, shape, otherShape)
|
||||
-- return "slide"
|
||||
-- end)
|
||||
-- print(actualX, actualY)
|
||||
-- movePlayer(e, dt)
|
||||
end
|
||||
time = time + dt
|
||||
end
|
||||
|
||||
function mode.keyreleased(key, scancode)
|
||||
function mode:draw()
|
||||
for _,e in pairs(self.entities) do
|
||||
e:draw()
|
||||
end
|
||||
slick.drawWorld(self.world)
|
||||
love.graphics.print(time, love.graphics.getWidth() / 2, love.graphics.getHeight()/ 2)
|
||||
end
|
||||
|
||||
function mode.keypressed(key, scancode, isrepeat)
|
||||
function mode:keyreleased(key, scancode)
|
||||
end
|
||||
|
||||
function mode.mousereleased(x, y, button, istouch, presses)
|
||||
function mode:keypressed(key, scancode, isrepeat)
|
||||
end
|
||||
|
||||
function mode:mousereleased(x, y, button, istouch, presses)
|
||||
end
|
||||
|
||||
return mode
|
||||
|
||||
25
game/love_src/src/system/racing_phy.lua
Normal file
25
game/love_src/src/system/racing_phy.lua
Normal file
@ -0,0 +1,25 @@
|
||||
local function accelerate(dt, gas_input, speed, max_speed, accel, min_speed)
|
||||
if (min_speed == nil) then
|
||||
min_speed = 0
|
||||
end
|
||||
local new_speed = speed + accel * dt * gas_input
|
||||
if (new_speed >= max_speed) then
|
||||
new_speed = max_speed
|
||||
elseif (new_speed <= min_speed) then
|
||||
new_speed = min_speed
|
||||
elseif(new_speed <= -max_speed) then
|
||||
new_speed = -max_speed
|
||||
end
|
||||
return new_speed
|
||||
end
|
||||
|
||||
local function drift(dt, pos_x, speed, max_speed, curve, centrifugal)
|
||||
local speed_percent = (speed/max_speed)
|
||||
local dx = dt * 2 * speed_percent -- at top speed, should be able to cross from left to right (-1 to 1) in 1 second
|
||||
return pos_x - (dx * speed_percent * curve * centrifugal);
|
||||
end
|
||||
|
||||
return {
|
||||
accelerate = accelerate,
|
||||
drift = drift
|
||||
}
|
||||
51
game/love_src/src/world/top_down_race/component/race.lua
Normal file
51
game/love_src/src/world/top_down_race/component/race.lua
Normal file
@ -0,0 +1,51 @@
|
||||
local components = {}
|
||||
|
||||
components.dict = {
|
||||
velocity = "race.velocity",
|
||||
max_speed = "race.max_speed",
|
||||
accel = "race.accel",
|
||||
grip = "race.grip",
|
||||
steer = "race.steer",
|
||||
brake = "race.brake",
|
||||
decel = "race.decel",
|
||||
drag = "race.drag",
|
||||
direction = "race.direction"
|
||||
}
|
||||
|
||||
function components.velocity (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.max_speed (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.accel (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.grip (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.steer (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.brake (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.decel (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.drag (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
function components.direction (c, x)
|
||||
c.data = x
|
||||
end
|
||||
|
||||
return components
|
||||
47
game/love_src/src/world/top_down_race/init.lua
Normal file
47
game/love_src/src/world/top_down_race/init.lua
Normal file
@ -0,0 +1,47 @@
|
||||
local reap = require("lib.reap")
|
||||
|
||||
local BASE = reap.base_path(...)
|
||||
|
||||
local world = require("love_src.wrapper.Concord.world")
|
||||
|
||||
local debug_entity = require("love_src.src.world.common.template.debug_entity")
|
||||
local racer = require("love_src.src.world.top_down_race.template.racer")
|
||||
local wm = require("world_map")
|
||||
|
||||
local wrapper = world:extend()
|
||||
|
||||
local name = "top_down_race"
|
||||
|
||||
function wrapper:new()
|
||||
wrapper.super.new(self, BASE, name)
|
||||
end
|
||||
|
||||
function wrapper:load(_args)
|
||||
wrapper.super.load(self, {
|
||||
"love_src/src/world/common/system/",
|
||||
"love_src/src/world/top_down_race/system/"
|
||||
}, {
|
||||
{
|
||||
assemblage = debug_entity.assembleDebug,
|
||||
data = {
|
||||
position = {0, 0},
|
||||
label = name
|
||||
}
|
||||
},
|
||||
{
|
||||
assemblage = racer.assemble,
|
||||
data = {
|
||||
accel = 10.0,
|
||||
brake = 10.0,
|
||||
grip = 10.0,
|
||||
max_speed = 100.0,
|
||||
steer = 10.0,
|
||||
velocity = 10.0,
|
||||
decel = 2.0,
|
||||
drag = 2.0
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
return wrapper
|
||||
125
game/love_src/src/world/top_down_race/system/velocity.lua
Normal file
125
game/love_src/src/world/top_down_race/system/velocity.lua
Normal file
@ -0,0 +1,125 @@
|
||||
local system_constructor = require("love_src.wrapper.Concord.system")
|
||||
local race = require("love_src.src.world.top_down_race.component.race")
|
||||
local racing_phy = require("love_src.src.system.racing_phy")
|
||||
|
||||
local component = require("love_src.wrapper.Concord.component")
|
||||
|
||||
local vm = require("lib.vornmath")
|
||||
|
||||
local system = {}
|
||||
|
||||
system.__index = system
|
||||
|
||||
system.pool = {
|
||||
pool = {
|
||||
race.dict.accel,
|
||||
race.dict.brake,
|
||||
race.dict.grip,
|
||||
race.dict.max_speed,
|
||||
race.dict.steer,
|
||||
race.dict.velocity,
|
||||
race.dict.decel,
|
||||
race.dict.drag
|
||||
}
|
||||
}
|
||||
|
||||
system.components = {
|
||||
[race.dict.accel] = race.accel,
|
||||
[race.dict.brake] = race.brake,
|
||||
[race.dict.grip] = race.grip,
|
||||
[race.dict.max_speed] = race.max_speed,
|
||||
[race.dict.steer] = race.steer,
|
||||
[race.dict.velocity] = race.velocity,
|
||||
[race.dict.decel] = race.decel,
|
||||
[race.dict.drag] = race.drag
|
||||
}
|
||||
|
||||
function system.new()
|
||||
local new_system = system_constructor.new("velocity", system.pool)
|
||||
if (new_system) then
|
||||
for k, v in pairs(system) do
|
||||
new_system[k] = v
|
||||
end
|
||||
return new_system
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function get(e)
|
||||
return
|
||||
e[race.dict.accel].data,
|
||||
e[race.dict.brake].data,
|
||||
e[race.dict.grip].data,
|
||||
e[race.dict.max_speed].data,
|
||||
e[race.dict.steer].data,
|
||||
e[race.dict.velocity].data,
|
||||
e[race.dict.drag].data,
|
||||
e[race.dict.decel].data
|
||||
end
|
||||
|
||||
local function draw(text, x, y, sx, sy, angle)
|
||||
love.graphics.push()
|
||||
love.graphics.scale(sx, sy)
|
||||
love.graphics.rotate(angle)
|
||||
love.graphics.print(text, x, y)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
function system:load()
|
||||
component.component("race.pos", function (c, x, y)
|
||||
c.data = vm.vec2(x, y)
|
||||
end)
|
||||
component.component("race.scale", function (c, x, y)
|
||||
c.data = vm.vec2(x, y)
|
||||
end)
|
||||
component.component("race.angle", function (c, x)
|
||||
c.data = x
|
||||
end)
|
||||
|
||||
for _, e in ipairs(self.pool) do
|
||||
e:give("race.pos", 10, 10)
|
||||
e:give("race.scale", 1, 1)
|
||||
e:give("race.angle", 0)
|
||||
end
|
||||
end
|
||||
|
||||
function system:update(dt)
|
||||
for _, e in ipairs(self.pool) do
|
||||
local accel, brake, grip, max_speed, steer, velocity, drag, decel = get(e)
|
||||
local up = love.keyboard.isDown("up")
|
||||
local down = love.keyboard.isDown("down")
|
||||
local left = love.keyboard.isDown("left")
|
||||
local right = love.keyboard.isDown("right")
|
||||
|
||||
if (up) then
|
||||
e[race.dict.velocity].data = racing_phy.accelerate(dt, 1, velocity, max_speed, accel - drag)
|
||||
elseif(down) then
|
||||
e[race.dict.velocity].data = racing_phy.accelerate(dt, -1, velocity, max_speed, accel - drag, -max_speed)
|
||||
else
|
||||
e[race.dict.velocity].data = racing_phy.accelerate(dt, 1, velocity, max_speed, -drag -decel)
|
||||
end
|
||||
|
||||
if (left) then
|
||||
e["race.angle"].data = e["race.angle"].data - 1 * dt
|
||||
end
|
||||
if (right) then
|
||||
e["race.angle"].data = e["race.angle"].data + 1 * dt
|
||||
end
|
||||
|
||||
velocity = e[race.dict.velocity].data
|
||||
|
||||
e["race.pos"].data = vm.vec2(e["race.pos"].data[1] + velocity, e["race.pos"].data[2])
|
||||
-- print(racing_phy.drift(dt, 1, velocity, max_speed, 2, 10))
|
||||
end
|
||||
end
|
||||
|
||||
function system:draw()
|
||||
for _, e in ipairs(self.pool) do
|
||||
local accel, brake, grip, max_speed, steer, velocity = get(e)
|
||||
local x, y, sx, sy, angle = e["race.pos"].data[1], e["race.pos"].data[2], e["race.scale"].data[1], e["race.scale"].data[2], e["race.angle"].data
|
||||
draw(velocity, x, y, sx, sy, angle)
|
||||
end
|
||||
end
|
||||
|
||||
return system
|
||||
26
game/love_src/src/world/top_down_race/template/racer.lua
Normal file
26
game/love_src/src/world/top_down_race/template/racer.lua
Normal file
@ -0,0 +1,26 @@
|
||||
local race = require("love_src.src.world.top_down_race.component.race")
|
||||
|
||||
local template = {}
|
||||
|
||||
template.default_data = {
|
||||
accel = 10.0,
|
||||
brake = 10.0,
|
||||
grip = 10.0,
|
||||
max_speed = 10.0,
|
||||
steer = 10.0,
|
||||
velocity = 10.0,
|
||||
decel = 5.0,
|
||||
drag = 2.0
|
||||
}
|
||||
function template.assemble(e, data)
|
||||
e:give(race.dict.accel, data.accel)
|
||||
e:give(race.dict.brake, data.brake)
|
||||
e:give(race.dict.grip, data.grip)
|
||||
e:give(race.dict.max_speed, data.max_speed)
|
||||
e:give(race.dict.steer, data.steer)
|
||||
e:give(race.dict.velocity, data.velocity)
|
||||
e:give(race.dict.decel, data.decel)
|
||||
e:give(race.dict.drag, data.drag)
|
||||
end
|
||||
|
||||
return template
|
||||
459
game/main.lua
459
game/main.lua
@ -1,226 +1,226 @@
|
||||
local ffi = require 'ffi'
|
||||
local joysticks
|
||||
-- local ffi = require 'ffi'
|
||||
-- local joysticks
|
||||
|
||||
function init()
|
||||
joysticks = love.joystick.getJoysticks()
|
||||
for i, joystick in ipairs(joysticks) do
|
||||
print(i, joystick:getName())
|
||||
end
|
||||
-- function init()
|
||||
-- joysticks = love.joystick.getJoysticks()
|
||||
-- for i, joystick in ipairs(joysticks) do
|
||||
-- print(i, joystick:getName())
|
||||
-- end
|
||||
|
||||
ffi.cdef[[
|
||||
void load(const char * source_path);
|
||||
void update_window(int width, int height);
|
||||
void draw();
|
||||
void update_keyboard(int up, int down, int left, int right,
|
||||
int w, int s, int a, int d,
|
||||
int t, int g, int f, int h,
|
||||
int i, int k, int j, int l);
|
||||
void update_mouse(int x, int y);
|
||||
void update_joystick(int joystick_index,
|
||||
float lx, float ly, float rx, float ry, float tl, float tr,
|
||||
int up, int down, int left, int right,
|
||||
int a, int b, int x, int y,
|
||||
int leftshoulder, int rightshoulder,
|
||||
int start);
|
||||
void update(float time);
|
||||
-- ffi.cdef[[
|
||||
-- void load(const char * source_path);
|
||||
-- void update_window(int width, int height);
|
||||
-- void draw();
|
||||
-- void update_keyboard(int up, int down, int left, int right,
|
||||
-- int w, int s, int a, int d,
|
||||
-- int t, int g, int f, int h,
|
||||
-- int i, int k, int j, int l);
|
||||
-- void update_mouse(int x, int y);
|
||||
-- void update_joystick(int joystick_index,
|
||||
-- float lx, float ly, float rx, float ry, float tl, float tr,
|
||||
-- int up, int down, int left, int right,
|
||||
-- int a, int b, int x, int y,
|
||||
-- int leftshoulder, int rightshoulder,
|
||||
-- int start);
|
||||
-- void update(float time);
|
||||
|
||||
int draw_font_start();
|
||||
int draw_font(int font_ix, char const * text, int x, int y);
|
||||
-- int draw_font_start();
|
||||
-- int draw_font(int font_ix, char const * text, int x, int y);
|
||||
|
||||
void draw_line_quad_start();
|
||||
void draw_line(int x1, int y1, int x2, int y2);
|
||||
void draw_set_color(float r, float g, float b);
|
||||
void draw_quad(int x1, int y1, int x2, int y2,
|
||||
int x3, int y3, int x4, int y4);
|
||||
]]
|
||||
local source_path = love.filesystem.getSource()
|
||||
test = ffi.load(source_path .. "/test.so")
|
||||
test.load(source_path)
|
||||
end
|
||||
-- void draw_line_quad_start();
|
||||
-- void draw_line(int x1, int y1, int x2, int y2);
|
||||
-- void draw_set_color(float r, float g, float b);
|
||||
-- void draw_quad(int x1, int y1, int x2, int y2,
|
||||
-- int x3, int y3, int x4, int y4);
|
||||
-- ]]
|
||||
-- local source_path = love.filesystem.getSource()
|
||||
-- test = ffi.load(source_path .. "/test.so")
|
||||
-- test.load(source_path)
|
||||
-- end
|
||||
|
||||
local update = function(time)
|
||||
for joystick_index, joystick in ipairs(joysticks) do
|
||||
if joystick_index > 8 then
|
||||
break
|
||||
end
|
||||
local lx = joystick:getGamepadAxis("leftx")
|
||||
local ly = joystick:getGamepadAxis("lefty")
|
||||
local rx = joystick:getGamepadAxis("rightx")
|
||||
local ry = joystick:getGamepadAxis("righty")
|
||||
local tl = joystick:getGamepadAxis("triggerleft")
|
||||
local tr = joystick:getGamepadAxis("triggerright")
|
||||
local up = joystick:isGamepadDown("dpup")
|
||||
local down = joystick:isGamepadDown("dpdown")
|
||||
local left = joystick:isGamepadDown("dpleft")
|
||||
local right = joystick:isGamepadDown("dpright")
|
||||
local a = joystick:isGamepadDown("a")
|
||||
local b = joystick:isGamepadDown("b")
|
||||
local x = joystick:isGamepadDown("x")
|
||||
local y = joystick:isGamepadDown("y")
|
||||
local leftshoulder = joystick:isGamepadDown("leftshoulder")
|
||||
local rightshoulder = joystick:isGamepadDown("rightshoulder")
|
||||
local start = joystick:isGamepadDown("start")
|
||||
--print("start", i, start)
|
||||
test.update_joystick(joystick_index - 1,
|
||||
lx, ly, rx, ry, tl, tr,
|
||||
up, down, left, right,
|
||||
a, b, x, y,
|
||||
leftshoulder, rightshoulder,
|
||||
start)
|
||||
end
|
||||
-- local update = function(time)
|
||||
-- for joystick_index, joystick in ipairs(joysticks) do
|
||||
-- if joystick_index > 8 then
|
||||
-- break
|
||||
-- end
|
||||
-- local lx = joystick:getGamepadAxis("leftx")
|
||||
-- local ly = joystick:getGamepadAxis("lefty")
|
||||
-- local rx = joystick:getGamepadAxis("rightx")
|
||||
-- local ry = joystick:getGamepadAxis("righty")
|
||||
-- local tl = joystick:getGamepadAxis("triggerleft")
|
||||
-- local tr = joystick:getGamepadAxis("triggerright")
|
||||
-- local up = joystick:isGamepadDown("dpup")
|
||||
-- local down = joystick:isGamepadDown("dpdown")
|
||||
-- local left = joystick:isGamepadDown("dpleft")
|
||||
-- local right = joystick:isGamepadDown("dpright")
|
||||
-- local a = joystick:isGamepadDown("a")
|
||||
-- local b = joystick:isGamepadDown("b")
|
||||
-- local x = joystick:isGamepadDown("x")
|
||||
-- local y = joystick:isGamepadDown("y")
|
||||
-- local leftshoulder = joystick:isGamepadDown("leftshoulder")
|
||||
-- local rightshoulder = joystick:isGamepadDown("rightshoulder")
|
||||
-- local start = joystick:isGamepadDown("start")
|
||||
-- --print("start", i, start)
|
||||
-- test.update_joystick(joystick_index - 1,
|
||||
-- lx, ly, rx, ry, tl, tr,
|
||||
-- up, down, left, right,
|
||||
-- a, b, x, y,
|
||||
-- leftshoulder, rightshoulder,
|
||||
-- start)
|
||||
-- end
|
||||
|
||||
local up = love.keyboard.isDown("up")
|
||||
local down = love.keyboard.isDown("down")
|
||||
local left = love.keyboard.isDown("left")
|
||||
local right = love.keyboard.isDown("right")
|
||||
local w = love.keyboard.isDown("w")
|
||||
local s = love.keyboard.isDown("s")
|
||||
local a = love.keyboard.isDown("a")
|
||||
local d = love.keyboard.isDown("d")
|
||||
local t = love.keyboard.isDown("t")
|
||||
local g = love.keyboard.isDown("g")
|
||||
local f = love.keyboard.isDown("f")
|
||||
local h = love.keyboard.isDown("h")
|
||||
local i = love.keyboard.isDown("i")
|
||||
local k = love.keyboard.isDown("k")
|
||||
local j = love.keyboard.isDown("j")
|
||||
local l = love.keyboard.isDown("l")
|
||||
test.update_keyboard(up, down, left, right,
|
||||
w, s, a, d,
|
||||
t, g, f, h,
|
||||
i, k, j, l);
|
||||
-- local up = love.keyboard.isDown("up")
|
||||
-- local down = love.keyboard.isDown("down")
|
||||
-- local left = love.keyboard.isDown("left")
|
||||
-- local right = love.keyboard.isDown("right")
|
||||
-- local w = love.keyboard.isDown("w")
|
||||
-- local s = love.keyboard.isDown("s")
|
||||
-- local a = love.keyboard.isDown("a")
|
||||
-- local d = love.keyboard.isDown("d")
|
||||
-- local t = love.keyboard.isDown("t")
|
||||
-- local g = love.keyboard.isDown("g")
|
||||
-- local f = love.keyboard.isDown("f")
|
||||
-- local h = love.keyboard.isDown("h")
|
||||
-- local i = love.keyboard.isDown("i")
|
||||
-- local k = love.keyboard.isDown("k")
|
||||
-- local j = love.keyboard.isDown("j")
|
||||
-- local l = love.keyboard.isDown("l")
|
||||
-- test.update_keyboard(up, down, left, right,
|
||||
-- w, s, a, d,
|
||||
-- t, g, f, h,
|
||||
-- i, k, j, l);
|
||||
|
||||
test.update(time)
|
||||
end
|
||||
-- test.update(time)
|
||||
-- end
|
||||
|
||||
local draw = function()
|
||||
test.draw()
|
||||
end
|
||||
-- local draw = function()
|
||||
-- test.draw()
|
||||
-- end
|
||||
|
||||
local nico_draw = function()
|
||||
----------------------------------------------------------------------
|
||||
-- font drawing
|
||||
----------------------------------------------------------------------
|
||||
-- local nico_draw = function()
|
||||
-- ----------------------------------------------------------------------
|
||||
-- -- font drawing
|
||||
-- ----------------------------------------------------------------------
|
||||
|
||||
-- call "draw_font_start()" prior each "group" of "draw_font()" calls
|
||||
--
|
||||
-- a "group" of draw_font() calls are back-to-back/consecutive,
|
||||
-- with no non-font drawing between them.
|
||||
--
|
||||
-- For example:
|
||||
-- -- call "draw_font_start()" prior each "group" of "draw_font()" calls
|
||||
-- --
|
||||
-- -- a "group" of draw_font() calls are back-to-back/consecutive,
|
||||
-- -- with no non-font drawing between them.
|
||||
-- --
|
||||
-- -- For example:
|
||||
|
||||
local font_ix = test.draw_font_start()
|
||||
local x = 512
|
||||
local y = 50
|
||||
y = y + test.draw_font(font_ix, "lua test", x, y)
|
||||
y = y + test.draw_font(font_ix, "cool", x, y)
|
||||
-- local font_ix = test.draw_font_start()
|
||||
-- local x = 512
|
||||
-- local y = 50
|
||||
-- y = y + test.draw_font(font_ix, "lua test", x, y)
|
||||
-- y = y + test.draw_font(font_ix, "cool", x, y)
|
||||
|
||||
-- note that "font_ix" is the current "best font" as calculated
|
||||
-- from the current window size, and might change next frame if the
|
||||
-- window is resized.
|
||||
--
|
||||
-- Any of this of course could be changed to match your precise
|
||||
-- requirements.
|
||||
-- -- note that "font_ix" is the current "best font" as calculated
|
||||
-- -- from the current window size, and might change next frame if the
|
||||
-- -- window is resized.
|
||||
-- --
|
||||
-- -- Any of this of course could be changed to match your precise
|
||||
-- -- requirements.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- line drawing
|
||||
----------------------------------------------------------------------
|
||||
-- ----------------------------------------------------------------------
|
||||
-- -- line drawing
|
||||
-- ----------------------------------------------------------------------
|
||||
|
||||
-- call "draw_line_quad_start()" prior to each "group" of
|
||||
-- "draw_line()" or "draw_quad()" calls
|
||||
--
|
||||
-- a "group" of draw_line()/draw_quad() calls are
|
||||
-- back-to-back/consecutive, with no non-line/quad drawing between
|
||||
-- them.
|
||||
--
|
||||
-- For example:
|
||||
-- -- call "draw_line_quad_start()" prior to each "group" of
|
||||
-- -- "draw_line()" or "draw_quad()" calls
|
||||
-- --
|
||||
-- -- a "group" of draw_line()/draw_quad() calls are
|
||||
-- -- back-to-back/consecutive, with no non-line/quad drawing between
|
||||
-- -- them.
|
||||
-- --
|
||||
-- -- For example:
|
||||
|
||||
test.draw_line_quad_start()
|
||||
test.draw_set_color(1.0, 0.0, 0.0) -- r, g, b (0.0 to 1.0)
|
||||
test.draw_line(0, 0, 1024, 1024) -- x1, y1, x2, y2
|
||||
test.draw_line(700, 300, 400, 500)
|
||||
test.draw_set_color(0.0, 1.0, 0.0)
|
||||
test.draw_line(700, 300, 400, 700)
|
||||
-- test.draw_line_quad_start()
|
||||
-- test.draw_set_color(1.0, 0.0, 0.0) -- r, g, b (0.0 to 1.0)
|
||||
-- test.draw_line(0, 0, 1024, 1024) -- x1, y1, x2, y2
|
||||
-- test.draw_line(700, 300, 400, 500)
|
||||
-- test.draw_set_color(0.0, 1.0, 0.0)
|
||||
-- test.draw_line(700, 300, 400, 700)
|
||||
|
||||
-- x1, y1, x2, y2,
|
||||
-- x3, y3, x4, y4,
|
||||
--
|
||||
-- vertices must be specified In "counter clockwise" order, as in:
|
||||
--
|
||||
-- 2──1
|
||||
-- │ │ valid (counter clockwise)
|
||||
-- 3──4
|
||||
--
|
||||
-- these can also be rotated, as in:
|
||||
--
|
||||
-- 3
|
||||
-- ╱ ╲
|
||||
-- 4 2 valid (counter clockwise)
|
||||
-- ╲ ╱
|
||||
-- 1
|
||||
--
|
||||
-- however "mirroring" is not valid, as in:
|
||||
--
|
||||
-- 1──2
|
||||
-- │ │ not valid (clockwise)
|
||||
-- 4──3
|
||||
--
|
||||
test.draw_set_color(0.0, 0.0, 1.0)
|
||||
test.draw_quad(
|
||||
600, 600, -- top right
|
||||
500, 600, -- top left
|
||||
500, 700, -- bottom left
|
||||
600, 700 -- bottom right
|
||||
)
|
||||
test.draw_set_color(0.0, 0.5, 1.0)
|
||||
test.draw_quad(
|
||||
900, 900, -- bottom
|
||||
950, 850, -- right
|
||||
900, 800, -- top
|
||||
850, 850 -- left
|
||||
)
|
||||
-- -- x1, y1, x2, y2,
|
||||
-- -- x3, y3, x4, y4,
|
||||
-- --
|
||||
-- -- vertices must be specified In "counter clockwise" order, as in:
|
||||
-- --
|
||||
-- -- 2──1
|
||||
-- -- │ │ valid (counter clockwise)
|
||||
-- -- 3──4
|
||||
-- --
|
||||
-- -- these can also be rotated, as in:
|
||||
-- --
|
||||
-- -- 3
|
||||
-- -- ╱ ╲
|
||||
-- -- 4 2 valid (counter clockwise)
|
||||
-- -- ╲ ╱
|
||||
-- -- 1
|
||||
-- --
|
||||
-- -- however "mirroring" is not valid, as in:
|
||||
-- --
|
||||
-- -- 1──2
|
||||
-- -- │ │ not valid (clockwise)
|
||||
-- -- 4──3
|
||||
-- --
|
||||
-- test.draw_set_color(0.0, 0.0, 1.0)
|
||||
-- test.draw_quad(
|
||||
-- 600, 600, -- top right
|
||||
-- 500, 600, -- top left
|
||||
-- 500, 700, -- bottom left
|
||||
-- 600, 700 -- bottom right
|
||||
-- )
|
||||
-- test.draw_set_color(0.0, 0.5, 1.0)
|
||||
-- test.draw_quad(
|
||||
-- 900, 900, -- bottom
|
||||
-- 950, 850, -- right
|
||||
-- 900, 800, -- top
|
||||
-- 850, 850 -- left
|
||||
-- )
|
||||
|
||||
-- If you want to draw a large number of lines or quads in bulk
|
||||
-- (e.g: 10,000+ lines/quads per frame), this interface might not be good
|
||||
-- enough, and we should discuss this in more detail.
|
||||
end
|
||||
-- -- If you want to draw a large number of lines or quads in bulk
|
||||
-- -- (e.g: 10,000+ lines/quads per frame), this interface might not be good
|
||||
-- -- enough, and we should discuss this in more detail.
|
||||
-- end
|
||||
|
||||
function love.run()
|
||||
init()
|
||||
-- function love.run()
|
||||
-- init()
|
||||
|
||||
return function()
|
||||
love.event.pump()
|
||||
for name, a,b,c,d,e,f,g,h in love.event.poll() do
|
||||
if name == "quit" then
|
||||
if c or not love.quit or not love.quit() then
|
||||
return a or 0, b
|
||||
end
|
||||
end
|
||||
end
|
||||
-- return function()
|
||||
-- love.event.pump()
|
||||
-- for name, a,b,c,d,e,f,g,h in love.event.poll() do
|
||||
-- if name == "quit" then
|
||||
-- if c or not love.quit or not love.quit() then
|
||||
-- return a or 0, b
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local width
|
||||
local height
|
||||
local flags
|
||||
width, height, flags = love.window.getMode()
|
||||
test.update_window(width, height)
|
||||
-- local width
|
||||
-- local height
|
||||
-- local flags
|
||||
-- width, height, flags = love.window.getMode()
|
||||
-- test.update_window(width, height)
|
||||
|
||||
local time = love.timer.getTime()
|
||||
update(time)
|
||||
-- local time = love.timer.getTime()
|
||||
-- update(time)
|
||||
|
||||
draw()
|
||||
-- draw()
|
||||
|
||||
local mouse_down = love.mouse.isDown(1)
|
||||
if mouse_down then
|
||||
local x, y = love.mouse.getPosition()
|
||||
test.update_mouse(x, y)
|
||||
end
|
||||
-- local mouse_down = love.mouse.isDown(1)
|
||||
-- if mouse_down then
|
||||
-- local x, y = love.mouse.getPosition()
|
||||
-- test.update_mouse(x, y)
|
||||
-- end
|
||||
|
||||
-- nico_draw()
|
||||
-- -- nico_draw()
|
||||
|
||||
love.graphics.present()
|
||||
love.timer.sleep(0.001)
|
||||
end
|
||||
end
|
||||
-- love.graphics.present()
|
||||
-- love.timer.sleep(0.001)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- function love.load(args)
|
||||
-- init()
|
||||
@ -248,43 +248,44 @@ end
|
||||
|
||||
-- end
|
||||
|
||||
-- local wm = require("world_map")
|
||||
local wm = require("world_map")
|
||||
|
||||
-- world = {
|
||||
-- ["main_menu"] = require("love_src.src.world.main_menu")(),
|
||||
-- ["1_intro"] = require("love_src.src.world.1_intro")(),
|
||||
-- ["2_town_square"] = require("love_src.src.world.2_town_square")(),
|
||||
-- ["race"] = require("love_src.src.world.race")(),
|
||||
-- ["train"] = require("love_src.src.world.train")(),
|
||||
-- };
|
||||
world = {
|
||||
["top_down_race"] = require("love_src.src.world.top_down_race")(),
|
||||
-- ["main_menu"] = require("love_src.src.world.main_menu")(),
|
||||
-- ["1_intro"] = require("love_src.src.world.1_intro")(),
|
||||
-- ["2_town_square"] = require("love_src.src.world.2_town_square")(),
|
||||
-- ["race"] = require("love_src.src.world.race")(),
|
||||
-- ["train"] = require("love_src.src.world.train")(),
|
||||
};
|
||||
|
||||
-- current = wm["2_town_square"]
|
||||
current = wm["top_down_race"]
|
||||
|
||||
-- function load_world(world_to_load)
|
||||
-- current = world_to_load
|
||||
-- world[current]:reload()
|
||||
-- end
|
||||
function load_world(world_to_load)
|
||||
current = world_to_load
|
||||
world[current]:reload()
|
||||
end
|
||||
|
||||
-- function love.load()
|
||||
-- world[current]:load()
|
||||
-- end
|
||||
function love.load()
|
||||
world[current]:load()
|
||||
end
|
||||
|
||||
-- function love.update(dt)
|
||||
-- world[current]:update(dt)
|
||||
-- end
|
||||
function love.update(dt)
|
||||
world[current]:update(dt)
|
||||
end
|
||||
|
||||
-- function love.draw()
|
||||
-- world[current]:draw()
|
||||
-- end
|
||||
function love.draw()
|
||||
world[current]:draw()
|
||||
end
|
||||
|
||||
-- function love.keyreleased(key, scancode)
|
||||
-- world[current]:keyreleased(key, scancode)
|
||||
-- end
|
||||
function love.keyreleased(key, scancode)
|
||||
world[current]:keyreleased(key, scancode)
|
||||
end
|
||||
|
||||
-- function love.keypressed(key, scancode, isrepeat)
|
||||
-- world[current]:keypressed(key, scancode, isrepeat)
|
||||
-- end
|
||||
function love.keypressed(key, scancode, isrepeat)
|
||||
world[current]:keypressed(key, scancode, isrepeat)
|
||||
end
|
||||
|
||||
-- function love.mousereleased(x, y, button, istouch, presses)
|
||||
-- world[current]:mousereleased(x, y, button, istouch, presses)
|
||||
-- end
|
||||
function love.mousereleased(x, y, button, istouch, presses)
|
||||
world[current]:mousereleased(x, y, button, istouch, presses)
|
||||
end
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
return {
|
||||
["top_down_race"] = "top_down_race",
|
||||
["main_menu"] = "main_menu",
|
||||
["1_intro"] = "1_intro",
|
||||
["2_town_square"] = "2_town_square",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user