Much much much more generic collision detection.
Now uses a small collidingEntities system essentially as just a query for the collisionDetection system. Makes a simple stack of collected ingredients at the end of a round. Added some hacks to the velocity system that I don't like! But it works somewhat splendidly!
This commit is contained in:
parent
cdb8c1315e
commit
0b79dc68fb
50
src/cart.lua
50
src/cart.lua
|
@ -1,3 +1,5 @@
|
||||||
|
import("plate.lua")
|
||||||
|
|
||||||
Cart = {}
|
Cart = {}
|
||||||
|
|
||||||
local sizeX, sizeY = CartSprite:getSize()
|
local sizeX, sizeY = CartSprite:getSize()
|
||||||
|
@ -17,8 +19,54 @@ function Cart.reset(o)
|
||||||
flat = { x = 0, y = 0 },
|
flat = { x = 0, y = 0 },
|
||||||
mult = { x = 1, y = 1 },
|
mult = { x = 1, y = 1 },
|
||||||
}
|
}
|
||||||
o.mass = T.marker
|
o.canCollideWith = 1
|
||||||
|
o.mass = 1
|
||||||
o.drawAsSprite = CartSprite
|
o.drawAsSprite = CartSprite
|
||||||
|
o.focusPriority = 0
|
||||||
|
|
||||||
|
---@type pd_image[]
|
||||||
|
o.collectables = {}
|
||||||
|
|
||||||
|
---@param world World
|
||||||
|
function o:spawnEntitiesWhenStopped(world)
|
||||||
|
local y = self.position.y - 240
|
||||||
|
local rectWidth = 150
|
||||||
|
local plateSize = { x = rectWidth, y = 10 }
|
||||||
|
self.velocity = { x = 300, y = 0 }
|
||||||
|
world:addEntity(self)
|
||||||
|
world:addEntity({
|
||||||
|
position = { x = self.position.x, y = self.position.y },
|
||||||
|
focusPriority = 1,
|
||||||
|
})
|
||||||
|
world:addEntity({
|
||||||
|
drawAsRectangle = { size = plateSize },
|
||||||
|
size = plateSize,
|
||||||
|
mass = 0.5,
|
||||||
|
velocity = { x = 0, y = 0 },
|
||||||
|
position = { x = self.position.x - (rectWidth / 2), y = y },
|
||||||
|
canCollideWith = 2,
|
||||||
|
canBeCollidedBy = 2,
|
||||||
|
isSolid = true,
|
||||||
|
stopMovingOnCollision = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, collectable in ipairs(self.collectables) do
|
||||||
|
local collX, collY = collectable:getSize()
|
||||||
|
y = y - collY - 5
|
||||||
|
world:addEntity({
|
||||||
|
drawAsSprite = collectable,
|
||||||
|
size = { x = collX, y = collY / 2 },
|
||||||
|
mass = 0.5,
|
||||||
|
velocity = { x = 0, y = 0 },
|
||||||
|
position = { x = self.position.x - (collX / 2), y = y },
|
||||||
|
canCollideWith = 2,
|
||||||
|
canBeCollidedBy = 2,
|
||||||
|
isSolid = true,
|
||||||
|
stopMovingOnCollision = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,12 @@ local canBounce = {
|
||||||
|
|
||||||
function Cheese.initialize(o, x, y)
|
function Cheese.initialize(o, x, y)
|
||||||
o.score = 5
|
o.score = 5
|
||||||
|
o.canBeCollidedBy = 1
|
||||||
o.expireAfterCollision = true
|
o.expireAfterCollision = true
|
||||||
o.size = size
|
o.size = size
|
||||||
o.position = { x = x, y = y }
|
o.position = { x = x, y = y }
|
||||||
o.drawAsSprite = CheeseSprite
|
o.drawAsSprite = CheeseSprite
|
||||||
|
o.collectable = o.drawAsSprite
|
||||||
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
||||||
o.canBounce = canBounce
|
o.canBounce = canBounce
|
||||||
newestBooster = o
|
newestBooster = o
|
||||||
|
|
|
@ -42,7 +42,7 @@ function Ingredients.getNext(x, y)
|
||||||
return Lettuce.initialize(o, x, y)
|
return Lettuce.initialize(o, x, y)
|
||||||
elseif odds > 0.2 then
|
elseif odds > 0.2 then
|
||||||
return Tomato.initialize(o, x, y)
|
return Tomato.initialize(o, x, y)
|
||||||
elseif odds > 0.1 then
|
elseif odds > 0.05 then
|
||||||
return Mushroom.initialize(o, x, 218 + math.random(1, 5))
|
return Mushroom.initialize(o, x, 218 + math.random(1, 5))
|
||||||
else
|
else
|
||||||
return Cheese.initialize(o, x, y)
|
return Cheese.initialize(o, x, y)
|
||||||
|
|
|
@ -11,10 +11,12 @@ local canBounce = {
|
||||||
|
|
||||||
function Lettuce.initialize(o, x, y)
|
function Lettuce.initialize(o, x, y)
|
||||||
o.score = 1
|
o.score = 1
|
||||||
|
o.canBeCollidedBy = 1
|
||||||
o.expireAfterCollision = true
|
o.expireAfterCollision = true
|
||||||
o.size = size
|
o.size = size
|
||||||
o.position = { x = x, y = y }
|
o.position = { x = x, y = y }
|
||||||
o.drawAsSprite = LettuceSprite
|
o.drawAsSprite = LettuceSprite
|
||||||
|
o.collectable = o.drawAsSprite
|
||||||
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
||||||
o.canBounce = canBounce
|
o.canBounce = canBounce
|
||||||
newestBooster = o
|
newestBooster = o
|
||||||
|
|
|
@ -11,10 +11,12 @@ local canBounce = {
|
||||||
|
|
||||||
function Mushroom.initialize(o, x, y)
|
function Mushroom.initialize(o, x, y)
|
||||||
o.score = 5
|
o.score = 5
|
||||||
|
o.canBeCollidedBy = 1
|
||||||
o.expireAfterCollision = true
|
o.expireAfterCollision = true
|
||||||
o.size = size
|
o.size = size
|
||||||
o.position = { x = x, y = y }
|
o.position = { x = x, y = y }
|
||||||
o.drawAsSprite = MushroomSprite
|
o.drawAsSprite = MushroomSprite
|
||||||
|
o.collectable = o.drawAsSprite
|
||||||
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
||||||
o.canBounce = canBounce
|
o.canBounce = canBounce
|
||||||
newestBooster = o
|
newestBooster = o
|
||||||
|
|
|
@ -6,10 +6,12 @@ local expireWhenOffScreenBy = { x = 2000, y = 480 }
|
||||||
|
|
||||||
function Tomato.initialize(o, x, y)
|
function Tomato.initialize(o, x, y)
|
||||||
o.score = 15
|
o.score = 15
|
||||||
|
o.canBeCollidedBy = 1
|
||||||
o.expireAfterCollision = true
|
o.expireAfterCollision = true
|
||||||
o.size = size
|
o.size = size
|
||||||
o.position = { x = x, y = y }
|
o.position = { x = x, y = y }
|
||||||
o.drawAsSprite = TomatoSprite
|
o.drawAsSprite = TomatoSprite
|
||||||
|
o.collectable = o.drawAsSprite
|
||||||
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
o.expireWhenOffScreenBy = expireWhenOffScreenBy
|
||||||
newestBooster = o
|
newestBooster = o
|
||||||
return newestBooster
|
return newestBooster
|
||||||
|
|
36
src/main.lua
36
src/main.lua
|
@ -1,18 +1,17 @@
|
||||||
-- stylua: ignore start
|
import("CoreLibs/animation.lua")
|
||||||
import 'CoreLibs/animation.lua'
|
import("CoreLibs/animator.lua")
|
||||||
import 'CoreLibs/animator.lua'
|
import("CoreLibs/easing.lua")
|
||||||
import 'CoreLibs/easing.lua'
|
import("CoreLibs/graphics.lua")
|
||||||
import 'CoreLibs/graphics.lua'
|
import("CoreLibs/object.lua")
|
||||||
import 'CoreLibs/object.lua'
|
import("CoreLibs/timer.lua")
|
||||||
import 'CoreLibs/timer.lua'
|
import("CoreLibs/ui.lua")
|
||||||
import 'CoreLibs/ui.lua'
|
import("CoreLibs/utilities/where.lua")
|
||||||
import 'CoreLibs/utilities/where.lua'
|
|
||||||
-- stylua: ignore end
|
|
||||||
|
|
||||||
import("../lib/tiny.lua")
|
import("../lib/tiny.lua")
|
||||||
import("tiny-tools.lua")
|
import("tiny-tools.lua")
|
||||||
import("assets.lua")
|
import("assets.lua")
|
||||||
import("systems/filter-types.lua")
|
import("systems/filter-types.lua")
|
||||||
|
import("systems/camera-pan.lua")
|
||||||
import("systems/collision-detection.lua")
|
import("systems/collision-detection.lua")
|
||||||
import("systems/collision-resolution.lua")
|
import("systems/collision-resolution.lua")
|
||||||
import("systems/draw.lua")
|
import("systems/draw.lua")
|
||||||
|
@ -31,6 +30,7 @@ local floorSize = { x = 10000, y = 10 }
|
||||||
floor = {
|
floor = {
|
||||||
position = { x = 0, y = 235 },
|
position = { x = 0, y = 235 },
|
||||||
size = floorSize,
|
size = floorSize,
|
||||||
|
canBeCollidedBy = 1 | 2,
|
||||||
canBounce = {
|
canBounce = {
|
||||||
flat = { x = 0, y = 0 },
|
flat = { x = 0, y = 0 },
|
||||||
mult = { x = 0.9, y = -0.5 },
|
mult = { x = 0.9, y = -0.5 },
|
||||||
|
@ -64,17 +64,17 @@ end
|
||||||
world = tiny.world(
|
world = tiny.world(
|
||||||
fallSystem,
|
fallSystem,
|
||||||
velocitySystem,
|
velocitySystem,
|
||||||
|
collidingEntities,
|
||||||
collisionResolution,
|
collisionResolution,
|
||||||
collisionDetection,
|
collisionDetection,
|
||||||
|
cameraPanSystem,
|
||||||
drawRectanglesSystem,
|
drawRectanglesSystem,
|
||||||
drawSpriteSystem,
|
drawSpriteSystem,
|
||||||
|
floor,
|
||||||
cart,
|
cart
|
||||||
floor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
local ingredientsEveryX = 50
|
local ingredientsEveryX = 90
|
||||||
|
|
||||||
local function init()
|
local function init()
|
||||||
for i = 1, Ingredients.cacheSize() do
|
for i = 1, Ingredients.cacheSize() do
|
||||||
|
@ -84,6 +84,9 @@ end
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
|
-- TODO: Re-enable when cart stops
|
||||||
|
playdate.setAutoLockDisabled(true)
|
||||||
|
|
||||||
function playdate.update()
|
function playdate.update()
|
||||||
local deltaSeconds = playdate.getElapsedTime()
|
local deltaSeconds = playdate.getElapsedTime()
|
||||||
playdate.resetElapsedTime()
|
playdate.resetElapsedTime()
|
||||||
|
@ -97,12 +100,11 @@ function playdate.update()
|
||||||
local panX = Camera.pan.x
|
local panX = Camera.pan.x
|
||||||
local rightEdge = panX + 400
|
local rightEdge = panX + 400
|
||||||
local offset = 600
|
local offset = 600
|
||||||
while newestX < (rightEdge + 100) do
|
while newestX < rightEdge + 100 do
|
||||||
newestX = newestX + ingredientsEveryX
|
newestX = newestX + ingredientsEveryX
|
||||||
world:addEntity(Ingredients.getNext(newestX, math.random(-500, 150)))
|
world:addEntity(Ingredients.getNext(newestX, math.random(-500, 150)))
|
||||||
end
|
end
|
||||||
floor.position.x = Camera.pan.x - offset
|
floor.position.x = Camera.pan.x - offset
|
||||||
gfx.setDrawOffset(-Camera.pan.x, -Camera.pan.y)
|
|
||||||
|
|
||||||
world:update(deltaSeconds)
|
world:update(deltaSeconds)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
local focusPriority = {}
|
||||||
|
|
||||||
|
cameraPanSystem = filteredSystem({ focusPriority = T.number, position = T.XyPair }, function(e, dt)
|
||||||
|
if e.focusPriority >= focusPriority.priority then
|
||||||
|
focusPriority.position = e.position
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
function cameraPanSystem:preProcess()
|
||||||
|
focusPriority.priority = 0
|
||||||
|
focusPriority.position = { x = 0, y = 0 }
|
||||||
|
end
|
||||||
|
|
||||||
|
function cameraPanSystem:postProcess()
|
||||||
|
local panX = math.max(0, focusPriority.position.x - 200)
|
||||||
|
local panY = math.min(0, focusPriority.position.y - 120)
|
||||||
|
gfx.setDrawOffset(-panX, -panY)
|
||||||
|
end
|
|
@ -1,27 +1,34 @@
|
||||||
|
collidingEntities = filteredSystem({
|
||||||
|
position = T.XyPair,
|
||||||
|
size = T.XyPair,
|
||||||
|
canCollideWith = T.bitMask,
|
||||||
|
isSolid = Maybe(T.bool),
|
||||||
|
})
|
||||||
|
|
||||||
collisionDetection = filteredSystem(
|
collisionDetection = filteredSystem(
|
||||||
{ position = T.XyPair, size = T.XyPair, isSolid = Maybe(T.bool) },
|
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.bitMask, isSolid = Maybe(T.bool) },
|
||||||
-- Here, the entity, e, refers to some entity that the cart global(!) may be colliding with.
|
-- Here, the entity, e, refers to some entity that a moving object may be colliding *into*
|
||||||
function(e, _, system)
|
function(e, _, system)
|
||||||
local collider = cart
|
for _, collider in pairs(collidingEntities.entities) do
|
||||||
if not collider.velocity then
|
if (e ~= collider) and collider.canCollideWith and ((collider.canCollideWith & e.canBeCollidedBy) ~= 0) then
|
||||||
return
|
local colliderTop = collider.position.y
|
||||||
end
|
local colliderBottom = collider.position.y + collider.size.y
|
||||||
local colliderTop = collider.position.y
|
local entityTop = e.position.y
|
||||||
local colliderBottom = collider.position.y + collider.size.y
|
local entityBottom = entityTop + e.size.y
|
||||||
local entityTop = e.position.y
|
|
||||||
local entityBottom = entityTop + e.size.y
|
|
||||||
|
|
||||||
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
|
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
|
||||||
or (entityBottom > colliderTop and entityBottom < colliderBottom)
|
or (entityBottom > colliderTop and entityBottom < colliderBottom)
|
||||||
|
|
||||||
if not withinY then
|
if
|
||||||
return
|
withinY
|
||||||
end
|
and collider.position.x < e.position.x + e.size.x
|
||||||
|
and collider.position.x + collider.size.x > e.position.x
|
||||||
if collider.position.x < e.position.x + e.size.x and collider.position.x + collider.size.x > e.position.x then
|
then
|
||||||
system.world:addEntity({ collisionBetween = { e, collider } })
|
system.world:addEntity({ collisionBetween = { e, collider } })
|
||||||
if e.expireAfterCollision then
|
if e.expireAfterCollision then
|
||||||
system.world:removeEntity(e)
|
system.world:removeEntity(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,37 +1,42 @@
|
||||||
collisionResolution = filteredSystem({ collisionBetween = T.Collision }, function(e, _, system)
|
collisionResolution = filteredSystem({ collisionBetween = T.Collision }, function(e, _, system)
|
||||||
local collider, probablyCart = e.collisionBetween[1], e.collisionBetween[2]
|
local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2]
|
||||||
local colliderTop = collider.position.y
|
local colliderTop = collidedInto.position.y
|
||||||
|
|
||||||
if collider.isSolid then
|
if collidedInto.isSolid then
|
||||||
-- Assumes impact from the top
|
-- Assumes impact from the top
|
||||||
probablyCart.position.y = colliderTop - probablyCart.size.y
|
collider.position.y = colliderTop - collider.size.y
|
||||||
end
|
end
|
||||||
|
|
||||||
if collider.canBounce and probablyCart.canBeBounced then
|
if collidedInto.canBounce and collider.canBeBounced then
|
||||||
probablyCart.velocity.x = probablyCart.velocity.x
|
collider.velocity.x = collider.velocity.x + collidedInto.canBounce.flat.x + collider.canBeBounced.flat.x
|
||||||
+ collider.canBounce.flat.x
|
|
||||||
+ probablyCart.canBeBounced.flat.x
|
|
||||||
|
|
||||||
probablyCart.velocity.x = probablyCart.velocity.x
|
collider.velocity.x = collider.velocity.x * collidedInto.canBounce.mult.x * collider.canBeBounced.mult.x
|
||||||
* collider.canBounce.mult.x
|
|
||||||
* probablyCart.canBeBounced.mult.x
|
|
||||||
|
|
||||||
-- abs() makes sure we always push upward
|
-- abs() makes sure we always push upward
|
||||||
probablyCart.velocity.y = math.abs(probablyCart.velocity.y)
|
collider.velocity.y = math.abs(collider.velocity.y)
|
||||||
+ collider.canBounce.flat.y
|
+ collidedInto.canBounce.flat.y
|
||||||
+ probablyCart.canBeBounced.flat.y
|
+ collider.canBeBounced.flat.y
|
||||||
|
|
||||||
probablyCart.velocity.y = probablyCart.velocity.y
|
collider.velocity.y = collider.velocity.y * collidedInto.canBounce.mult.y * collider.canBeBounced.mult.y
|
||||||
* collider.canBounce.mult.y
|
|
||||||
* probablyCart.canBeBounced.mult.y
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if collider.score then
|
if collider.stopMovingOnCollision then
|
||||||
Score:add(collider.score)
|
print("stopMovingOnCollision!")
|
||||||
|
collider.velocity = nil
|
||||||
|
collider.canCollideWith = nil
|
||||||
|
system.world:addEntity(collider)
|
||||||
end
|
end
|
||||||
|
|
||||||
if collider.expireAfterCollision then
|
if collidedInto.score then
|
||||||
system.world:removeEntity(collider)
|
Score:add(collidedInto.score)
|
||||||
|
end
|
||||||
|
|
||||||
|
if collidedInto.expireAfterCollision then
|
||||||
|
system.world:removeEntity(collidedInto)
|
||||||
|
end
|
||||||
|
|
||||||
|
if collidedInto.collectable and collider.collectables then
|
||||||
|
collider.collectables[#collider.collectables + 1] = collidedInto.collectable
|
||||||
end
|
end
|
||||||
|
|
||||||
system.world:removeEntity(e)
|
system.world:removeEntity(e)
|
||||||
|
|
|
@ -6,4 +6,4 @@ end)
|
||||||
|
|
||||||
drawSpriteSystem = filteredSystem({ position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system)
|
drawSpriteSystem = filteredSystem({ position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system)
|
||||||
e.drawAsSprite:draw(e.position.x, e.position.y)
|
e.drawAsSprite:draw(e.position.x, e.position.y)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -10,10 +10,14 @@ local Entity = {}
|
||||||
---@type XyPair
|
---@type XyPair
|
||||||
local XyPair = { x = 1, y = 1 }
|
local XyPair = { x = 1, y = 1 }
|
||||||
|
|
||||||
|
---@alias BitMask number
|
||||||
|
|
||||||
T = {
|
T = {
|
||||||
XyPair = XyPair,
|
XyPair = XyPair,
|
||||||
bool = true,
|
bool = true,
|
||||||
number = 0,
|
number = 0,
|
||||||
|
---@type BitMask
|
||||||
|
bitMask = 0,
|
||||||
numberArray = { 1, 2, 3 },
|
numberArray = { 1, 2, 3 },
|
||||||
str = "",
|
str = "",
|
||||||
marker = {},
|
marker = {},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
local G = -300
|
local G = -300
|
||||||
fallSystem = filteredSystem({ velocity = T.XyPair, mass = T.marker }, function(e, dt)
|
fallSystem = filteredSystem({ velocity = T.XyPair, mass = T.number }, function(e, dt)
|
||||||
e.velocity.y = e.velocity.y - (G * dt) - (0.5 * dt * dt)
|
e.velocity.y = e.velocity.y - (G * dt * e.mass) - (0.5 * dt * dt)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
local sqrt = math.sqrt
|
local sqrt = math.sqrt
|
||||||
|
|
||||||
velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt)
|
velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt, system)
|
||||||
if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 0.1 then
|
if not e.velocity then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 1 then
|
||||||
e.velocity = nil
|
e.velocity = nil
|
||||||
world:addEntity(e)
|
if e.spawnEntitiesWhenStopped then
|
||||||
|
e:spawnEntitiesWhenStopped(system.world)
|
||||||
|
e.spawnEntitiesWhenStopped = nil
|
||||||
|
end
|
||||||
|
system.world:addEntity(e)
|
||||||
else
|
else
|
||||||
e.position.x = e.position.x + (e.velocity.x * dt)
|
e.position.x = e.position.x + (e.velocity.x * dt)
|
||||||
e.position.y = e.position.y + (e.velocity.y * dt)
|
e.position.y = e.position.y + (e.velocity.y * dt)
|
||||||
|
|
|
@ -12,7 +12,7 @@ local isSimulator = playdate.isSimulator
|
||||||
---@generic T
|
---@generic T
|
||||||
---@param shape T
|
---@param shape T
|
||||||
---@param process fun(entity: T, dt: number, system: System)
|
---@param process fun(entity: T, dt: number, system: System)
|
||||||
---@return
|
---@return System | { entities: T[] }
|
||||||
function filteredSystem(shape, process)
|
function filteredSystem(shape, process)
|
||||||
local system = tiny.processingSystem()
|
local system = tiny.processingSystem()
|
||||||
local keys = {}
|
local keys = {}
|
||||||
|
@ -23,6 +23,9 @@ function filteredSystem(shape, process)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
system.filter = tiny.requireAll(table.unpack(keys))
|
system.filter = tiny.requireAll(table.unpack(keys))
|
||||||
|
if not process then
|
||||||
|
return system
|
||||||
|
end
|
||||||
if isSimulator then
|
if isSimulator then
|
||||||
-- local acceptableKeys = ""
|
-- local acceptableKeys = ""
|
||||||
-- for _, key in ipairs(keys) do
|
-- for _, key in ipairs(keys) do
|
||||||
|
|
Loading…
Reference in New Issue