Compare commits
2 Commits
823092ad95
...
76e61ed233
| Author | SHA1 | Date | |
|---|---|---|---|
| 76e61ed233 | |||
| 1dc045cbed |
@ -45,6 +45,8 @@ OBJS = \
|
||||
src/collada/effect.o \
|
||||
src/collada/node_state.o \
|
||||
src/collada/animate.o \
|
||||
src/lua_api.o \
|
||||
src/pixel_line_art.o \
|
||||
data/scenes/ship20/ship20.o \
|
||||
data/scenes/noodle/noodle.o \
|
||||
data/scenes/shadow_test/shadow_test.o \
|
||||
|
||||
@ -64,5 +64,5 @@ namespace font {
|
||||
void load_fonts(font * const fonts, font_desc const * const descs, int length);
|
||||
int best_font(font_desc const * const descs, int length);
|
||||
void draw_start(font const& font, unsigned int vertex_array_object, unsigned int index_buffer);
|
||||
void draw_string(font const& font, char const * const s, int x, int y);
|
||||
int draw_string(font const& font, char const * const s, int x, int y);
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ namespace lighting {
|
||||
|
||||
extern light_parameters global;
|
||||
|
||||
void load_program();
|
||||
void load_light_uniform_buffer();
|
||||
void draw();
|
||||
void load();
|
||||
void draw(unsigned int light_uniform_buffer, int light_count);
|
||||
}
|
||||
|
||||
18
game/include/lua_api.h
Normal file
18
game/include/lua_api.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -8,13 +8,15 @@ short index_buffer_configuration_offsets[] = {
|
||||
768, 780, 798, 816, 840, 858, 882, 906,
|
||||
936, 954, 978, 1002, 1032, 1056, 1086, 1116,
|
||||
};
|
||||
// generated from
|
||||
struct {
|
||||
short offset;
|
||||
short count;
|
||||
} index_buffer_custom_offsets[] = {
|
||||
{1152, 12}, // tallgrass.obj
|
||||
{1164, 36}, // fence.obj
|
||||
{1200, 36}, // torch.obj
|
||||
{1236, 24}, // wheat.obj
|
||||
{1260, 1584}, // custom-mushroom.obj
|
||||
{1152, 3180}, // candle.obj
|
||||
{4332, 1584}, // custom_mushroom.obj
|
||||
{5916, 36}, // fence.obj
|
||||
{5952, 24}, // stair.obj
|
||||
{5976, 12}, // tall_grass.obj
|
||||
{5988, 2082}, // wall_torch.obj
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ extern "C" {
|
||||
const char * geometry_path,
|
||||
const char * fragment_path);
|
||||
|
||||
unsigned int load_uniform_buffer(char const * const path);
|
||||
unsigned int load_uniform_buffer(char const * const path, int * out_size);
|
||||
|
||||
void load_dds_texture_2D(char const * const path);
|
||||
|
||||
|
||||
12
game/include/pixel_line_art.h
Normal file
12
game/include/pixel_line_art.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace pixel_line_art {
|
||||
|
||||
void load();
|
||||
|
||||
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);
|
||||
}
|
||||
@ -6,6 +6,8 @@ extern "C" {
|
||||
|
||||
void load(const char * source_path);
|
||||
void draw();
|
||||
void love2d_state_load();
|
||||
void love2d_state_restore();
|
||||
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,
|
||||
|
||||
@ -25,8 +25,8 @@ namespace world {
|
||||
};
|
||||
};
|
||||
|
||||
// also update index_buffer_custom_offsets in data.inc
|
||||
const int custom_block_types = 5;
|
||||
// also update index_buffer_custom_offsets in include/minecraft_data.inc
|
||||
const int custom_block_types = 6;
|
||||
const int instance_cfg_length = 64 + custom_block_types;
|
||||
|
||||
struct instance_cfg_entry {
|
||||
@ -42,8 +42,9 @@ namespace world {
|
||||
struct state {
|
||||
world::descriptor const * descriptor;
|
||||
world::region * region; // malloc region_count
|
||||
unsigned int light_uniform_buffer;
|
||||
entry_table::global_entry_t * entry_table;
|
||||
unsigned int light_uniform_buffer;
|
||||
int light_count;
|
||||
int entry_table_length;
|
||||
};
|
||||
|
||||
|
||||
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
|
||||
293
game/main.lua
293
game/main.lua
@ -1,107 +1,193 @@
|
||||
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);
|
||||
]]
|
||||
local source_path = love.filesystem.getSource()
|
||||
test = ffi.load(source_path .. "/test.so")
|
||||
test.load(source_path)
|
||||
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);
|
||||
|
||||
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
|
||||
-- int draw_font_start();
|
||||
-- int draw_font(int font_ix, char const * text, int x, int y);
|
||||
|
||||
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);
|
||||
-- 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
|
||||
|
||||
test.update(time)
|
||||
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 draw = function()
|
||||
test.draw()
|
||||
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 love_draw = function ()
|
||||
love.graphics.setCanvas()
|
||||
love.graphics.setShader()
|
||||
love.graphics.push()
|
||||
love.graphics.setColor(1,0,0,1)
|
||||
love.graphics.print("A", 100, 100)
|
||||
love.graphics.pop()
|
||||
end
|
||||
-- test.update(time)
|
||||
-- end
|
||||
|
||||
-- local draw = function()
|
||||
-- test.draw()
|
||||
-- end
|
||||
|
||||
-- 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:
|
||||
|
||||
-- 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.
|
||||
|
||||
-- ----------------------------------------------------------------------
|
||||
-- -- 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:
|
||||
|
||||
-- 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
|
||||
-- )
|
||||
|
||||
-- -- 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()
|
||||
|
||||
-- --love.timer.step()
|
||||
|
||||
-- return function()
|
||||
-- love.event.pump()
|
||||
-- for name, a,b,c,d,e,f,g,h in love.event.poll() do
|
||||
@ -118,7 +204,6 @@ end
|
||||
-- width, height, flags = love.window.getMode()
|
||||
-- test.update_window(width, height)
|
||||
|
||||
-- --local dt = love.timer.step()
|
||||
-- local time = love.timer.getTime()
|
||||
-- update(time)
|
||||
|
||||
@ -130,13 +215,10 @@ end
|
||||
-- test.update_mouse(x, y)
|
||||
-- end
|
||||
|
||||
-- print(love)
|
||||
-- love_draw()
|
||||
-- -- nico_draw()
|
||||
|
||||
-- love.graphics.present()
|
||||
-- love.timer.sleep(0.001)
|
||||
-- --love.timer.sleep(0.1)
|
||||
-- --local fps = love.timer.getFPS( )
|
||||
-- --print(fps)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
@ -169,14 +251,15 @@ end
|
||||
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")(),
|
||||
["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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -5,16 +5,13 @@ Wooden Planks,5,,4,,,,"""linear texture index"" is invalid if unspecified"
|
||||
Trunk,17,,5,,,,"""properties"" is (empty) if unspecified"
|
||||
Trunk,17,1,5,,,,
|
||||
Birch Block,17,2,6,,,,
|
||||
Leaves,18,,12,,,,
|
||||
Leaves,18,1,12,,,,
|
||||
Leaves,18,2,12,,,,
|
||||
Leaves,18,3,12,,,,
|
||||
Leaves,18,"0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15",12,,,,
|
||||
Unused,20,,11,,,,
|
||||
Door Bottom,21,,18,,,,
|
||||
Door Top,22,,17,,,,
|
||||
Tall Grass,31,"0,1",22,tall_grass,,,
|
||||
Tall Grass,31,"0,1",22,tall_grass,two_sided,,
|
||||
Tan Block 1,35,1,2,,,,
|
||||
White Block 2,35,5,6,,,,if you say so
|
||||
White Block 2,35,5,6,,,,
|
||||
Arch Bottom,35,6,10,,,,
|
||||
Tan Block 2,35,8,3,,,,
|
||||
White Block 1,35,9,21,,,,
|
||||
@ -23,13 +20,14 @@ Unused,35,10,,,,,
|
||||
Arch Top,35,14,9,,,,
|
||||
Grey Bricks 1,35,15,8,,,,
|
||||
White Block 3,35,,7,,,,
|
||||
Lilac Grass,37,,15,tall_grass,,,
|
||||
Spider Plant,38,,13,tall_grass,,,
|
||||
Spider Plant,39,,13,tall_grass,,,
|
||||
Lilac Grass,37,,15,tall_grass,two_sided,,
|
||||
Spider Plant,38,,13,tall_grass,two_sided,,
|
||||
Spider Plant,39,,13,tall_grass,two_sided,,
|
||||
Red Mushroom,40,,14,custom_mushroom,,,
|
||||
Start/Finish Line,45,,16,,,,
|
||||
Shadow Block,49,,20,,,,
|
||||
Wall Torch,50,"1,2,3,4",,wall_torch,emits_light,,
|
||||
Candle,50,5,,candle,emits_light,,
|
||||
Wall Torch,50,"1,2,3,4",62,wall_torch,"emits_light,torch_oriented",,
|
||||
Candle,50,5,61,candle,emits_light,,
|
||||
Stair,53,"0,1,2,3,4,5,6,7",4,stair,stair_oriented,,
|
||||
Fence,85,0,4,fence,,,
|
||||
Lamp Block,89,,19,,,,
|
||||
|
@ -77,5 +77,17 @@ def get_texture_id(block_id, block_data):
|
||||
decl = by_id_data.get((block_id, block_data), default_decl)
|
||||
return decl.linear_texture_index
|
||||
|
||||
def get_special(block_id, block_data):
|
||||
decl = by_id_data.get((block_id, block_data), default_decl)
|
||||
if "two_sided" in decl.properties:
|
||||
return -1
|
||||
if "torch_oriented" in decl.properties:
|
||||
return 1
|
||||
if "stair_oriented" in decl.properties:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(sorted_custom_mesh)
|
||||
from pprint import pprint
|
||||
#print(sorted_custom_mesh)
|
||||
pprint(sorted_decls)
|
||||
|
||||
2639
game/minecraft/gen/candle.obj
Normal file
2639
game/minecraft/gen/candle.obj
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,24 @@
|
||||
set -eux
|
||||
#cd ./minecraft/gen
|
||||
PYTHON=pypy3.11
|
||||
cat <<EOF > ../grandlecturn/all_regions.txt
|
||||
|
||||
CROP=-200,-150:300,200
|
||||
ALL_REGIONS=../grandlecturn/all_regions.txt
|
||||
|
||||
cat <<EOF > $ALL_REGIONS
|
||||
$HOME/GrandLecturn/region/r.0.0.mcr
|
||||
$HOME/GrandLecturn/region/r.-1.-1.mcr
|
||||
$HOME/GrandLecturn/region/r.0.-1.mcr
|
||||
$HOME/GrandLecturn/region/r.-1.0.mcr
|
||||
EOF
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.0.0.mcr ../grandlecturn/region.0.0 ../grandlecturn/all_regions.txt &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.-1.-1.mcr ../grandlecturn/region.-1.-1 ../grandlecturn/all_regions.txt &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.0.-1.mcr ../grandlecturn/region.0.-1 ../grandlecturn/all_regions.txt &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.-1.0.mcr ../grandlecturn/region.-1.0 ../grandlecturn/all_regions.txt &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.0.0.mcr ../grandlecturn/region.0.0 $ALL_REGIONS $CROP &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.-1.-1.mcr ../grandlecturn/region.-1.-1 $ALL_REGIONS $CROP &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.0.-1.mcr ../grandlecturn/region.0.-1 $ALL_REGIONS $CROP &
|
||||
$PYTHON mc.py $HOME/GrandLecturn/region/r.-1.0.mcr ../grandlecturn/region.-1.0 $ALL_REGIONS $CROP &
|
||||
|
||||
wait
|
||||
|
||||
cat ../grandlecturn/region*.lights.vtx > ../grandlecturn/global.lights.vtx
|
||||
|
||||
cat ../grandlecturn/region*.dump > ../grandlecturn/global.dump
|
||||
$PYTHON intkeys.py ../grandlecturn/global.dump | $HOME/nbperf/nbperf -n grandlecturn_hash -I -a bpz -c 1.26 -m ../love2dworld/map.txt > ../grandlecturn/inthash.c
|
||||
$PYTHON intkeys.py ../grandlecturn/global.dump | $HOME/nbperf/nbperf -n grandlecturn_hash -I -a bpz -c 1.24 -m ../love2dworld/map.txt > ../grandlecturn/inthash.c
|
||||
|
||||
@ -42,7 +42,15 @@ def neighbor_exists(level_table, chunk_x, chunk_z, nx, ny, nz):
|
||||
n_block_data = decode_block_data(level_table, chunk_x, chunk_z, n_block_index)
|
||||
return block_ids.is_neighbor_block(n_block_id, n_block_data)
|
||||
|
||||
def block_neighbors(level_table, chunk_x, chunk_z, block_index):
|
||||
def outside_crop(position, crop):
|
||||
return (
|
||||
position[0] < crop[0][0] or
|
||||
position[2] < crop[0][1] or
|
||||
position[0] > crop[1][0] or
|
||||
position[2] > crop[1][1]
|
||||
)
|
||||
|
||||
def block_neighbors(level_table, chunk_x, chunk_z, block_index, crop):
|
||||
block_id = level_table[(chunk_x, chunk_z)].blocks[block_index]
|
||||
if block_id == data.BlockID.AIR or block_id == data.BlockID.BEDROCK:
|
||||
return
|
||||
@ -51,6 +59,8 @@ def block_neighbors(level_table, chunk_x, chunk_z, block_index):
|
||||
|
||||
xyz = mcregion.xyz_from_block_index(block_index)
|
||||
center_position = vec3.add(xyz, (chunk_x * 16, 0, chunk_z * 16))
|
||||
if outside_crop(center_position, crop):
|
||||
return # block is cropped
|
||||
|
||||
if not block_ids.is_cube_block(block_id, block_data):
|
||||
yield center_position, block_id, block_data, None
|
||||
@ -66,10 +76,10 @@ def block_neighbors(level_table, chunk_x, chunk_z, block_index):
|
||||
if normal_indices:
|
||||
yield center_position, block_id, block_data, normal_indices
|
||||
|
||||
def devoxelize_region(level_table, level_table_keys):
|
||||
def devoxelize_region(level_table, level_table_keys, crop):
|
||||
for chunk_x, chunk_z in level_table_keys:
|
||||
for block_index in range(128 * 16 * 16):
|
||||
yield from block_neighbors(level_table, chunk_x, chunk_z, block_index)
|
||||
yield from block_neighbors(level_table, chunk_x, chunk_z, block_index, crop)
|
||||
|
||||
def build_level_table(level_table, mem, locations):
|
||||
for location in locations:
|
||||
@ -96,9 +106,11 @@ def build_block_configuration_table():
|
||||
yield indices
|
||||
|
||||
def pack_instance_data(position, block_id, block_data, texture_id):
|
||||
special = block_ids.get_special(block_id, block_data)
|
||||
|
||||
packed = struct.pack("<hhhhhhhh",
|
||||
position[0], position[1], position[2], 0,
|
||||
block_id, block_data, texture_id, 0)
|
||||
block_id, block_data, texture_id, special)
|
||||
return packed
|
||||
|
||||
def pack_light_data(position, block_id):
|
||||
@ -116,8 +128,8 @@ def build_block_instances(blocks):
|
||||
if block_ids.is_light_source(block_id, block_data):
|
||||
light_sources.append((position, block_id, block_data))
|
||||
if not block_ids.is_cube_block(block_id, block_data):
|
||||
#custom_mesh_index = block_ids.get_custom_mesh_index(block_id, block_data)
|
||||
#non_cube_blocks[custom_mesh_index].append((position, block_id, block_data))
|
||||
custom_mesh_index = block_ids.get_custom_mesh_index(block_id, block_data)
|
||||
non_cube_blocks[custom_mesh_index].append((position, block_id, block_data))
|
||||
continue
|
||||
configuration = normal_indices_as_block_configuration(normal_indices)
|
||||
by_configuration[configuration].append((position, block_id, block_data))
|
||||
@ -210,8 +222,9 @@ def dump_blocks(blocks):
|
||||
assert(len(buf) == 8)
|
||||
f.write(buf)
|
||||
|
||||
def main2(level_table, level_table_keys):
|
||||
blocks = devoxelize_region(level_table, level_table_keys)
|
||||
def main2(level_table, level_table_keys, crop):
|
||||
print("crop", crop)
|
||||
blocks = devoxelize_region(level_table, level_table_keys, crop)
|
||||
blocks = list(blocks)
|
||||
dump_blocks(blocks)
|
||||
build_block_instances(blocks)
|
||||
@ -222,7 +235,15 @@ def parse_all_paths(path):
|
||||
buf = f.read()
|
||||
return set(l.strip() for l in buf.split('\n') if l.strip())
|
||||
|
||||
def main(mcr_path, data_path, all_paths_path):
|
||||
def parse_crop(crop):
|
||||
min_xz, max_xz = crop.strip().split(":")
|
||||
min_xz = tuple(int(c) for c in min_xz.split(","))
|
||||
max_xz = tuple(int(c) for c in max_xz.split(","))
|
||||
assert min_xz[0] < max_xz[0], crop
|
||||
assert min_xz[1] < max_xz[1], crop
|
||||
return min_xz, max_xz
|
||||
|
||||
def main(mcr_path, data_path, all_paths_path, crop):
|
||||
all_paths = parse_all_paths(all_paths_path)
|
||||
assert mcr_path in all_paths
|
||||
level_table = {}
|
||||
@ -232,10 +253,10 @@ def main(mcr_path, data_path, all_paths_path):
|
||||
if path == mcr_path:
|
||||
continue
|
||||
level_table_from_path(level_table, path)
|
||||
|
||||
main2(level_table, level_table_keys)
|
||||
main2(level_table, level_table_keys, parse_crop(crop))
|
||||
|
||||
mcr_path = sys.argv[1]
|
||||
data_path = sys.argv[2]
|
||||
all_paths_path = sys.argv[3]
|
||||
main(mcr_path, data_path, all_paths_path)
|
||||
crop = sys.argv[4]
|
||||
main(mcr_path, data_path, all_paths_path, crop)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Blender 4.5.7 LTS
|
||||
# Blender 5.0.0
|
||||
# www.blender.org
|
||||
o Cube
|
||||
v 1.000000 -1.000000 -1.000000
|
||||
@ -5,6 +5,8 @@ import obj_state
|
||||
import obj_write
|
||||
import sys
|
||||
|
||||
import block_ids
|
||||
|
||||
normals = [
|
||||
(-1.0, 0.0, 0.0),
|
||||
(0.0, -1.0, 0.0),
|
||||
@ -49,11 +51,10 @@ def main():
|
||||
|
||||
build_configuration_index_buffers(cube_faces_by_normal, index_buffer)
|
||||
# check mc.py `custom_blocks` for model order
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, "tallgrass.obj")
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, "fence.obj")
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, "torch.obj")
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, "wheat.obj")
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, "custom-mushroom.obj")
|
||||
|
||||
mesh_order = block_ids.sorted_custom_mesh
|
||||
for mesh_name in mesh_order:
|
||||
obj_write.write_obj(vertex_buffer, index_buffer, index_lookup, f"{mesh_name}.obj")
|
||||
|
||||
with open("../configuration.idx", "wb") as f:
|
||||
obj_write.write_indices(f, "<H", index_buffer)
|
||||
|
||||
1507
game/minecraft/gen/wall_torch.obj
Normal file
1507
game/minecraft/gen/wall_torch.obj
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user