Add directional sticky and skewer box variants
This commit is contained in:
parent
c3f5aafd77
commit
f549eaf523
13 changed files with 723 additions and 101 deletions
353
tests/test_movement.lua
Normal file
353
tests/test_movement.lua
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
package.path = "./?.lua;" .. package.path
|
||||
|
||||
local Movement = require "movement"
|
||||
|
||||
local passed = 0
|
||||
|
||||
local function test(name, fn)
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then
|
||||
io.stderr:write("FAIL: " .. name .. "\n" .. err .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
passed = passed + 1
|
||||
end
|
||||
|
||||
local function assertTrue(value, message)
|
||||
if not value then error(message or "expected true", 2) end
|
||||
end
|
||||
|
||||
local function assertFalse(value, message)
|
||||
if value then error(message or "expected false", 2) end
|
||||
end
|
||||
|
||||
local function assertIn(group, entity, message)
|
||||
if group[entity] ~= entity then error(message or "expected entity in movement group", 2) end
|
||||
end
|
||||
|
||||
local function assertNotIn(group, entity, message)
|
||||
if group[entity] then error(message or "expected entity outside movement group", 2) end
|
||||
end
|
||||
|
||||
local function movable(x, y, sticky, shape, skewer)
|
||||
local entity = {
|
||||
x = x, y = y,
|
||||
sticky = sticky or false,
|
||||
skewer = skewer or false,
|
||||
shape = shape or {{x = 0, y = 0}},
|
||||
}
|
||||
function entity:canMove() return true end
|
||||
function entity:canPassOver() return false end
|
||||
function entity:isSticky() return self.sticky end
|
||||
function entity:isSkewer() return self.skewer end
|
||||
function entity:bump() end
|
||||
function entity:move(axis, amount) self[axis] = self[axis] + amount end
|
||||
function entity:getGridPos() return {x = self.x, y = self.y} end
|
||||
function entity:getOccupiedCells(offset)
|
||||
offset = offset or {x = 0, y = 0}
|
||||
local cells = {}
|
||||
for _, cell in ipairs(self.shape) do
|
||||
table.insert(cells, {x = self.x + cell.x + offset.x, y = self.y + cell.y + offset.y})
|
||||
end
|
||||
return cells
|
||||
end
|
||||
return entity
|
||||
end
|
||||
|
||||
local function wall(x, y)
|
||||
local entity = movable(x, y)
|
||||
function entity:canMove() return false end
|
||||
return entity
|
||||
end
|
||||
|
||||
local function world(entities, width, height)
|
||||
width, height = width or 8, height or 8
|
||||
local cells = {}
|
||||
for _, entity in ipairs(entities) do
|
||||
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||
cells[cell.y .. ":" .. cell.x] = entity
|
||||
end
|
||||
end
|
||||
return function(cell)
|
||||
return cells[cell.y .. ":" .. cell.x]
|
||||
end, function(cell)
|
||||
return cell.x >= 1 and cell.x <= width and cell.y >= 1 and cell.y <= height
|
||||
end
|
||||
end
|
||||
|
||||
local function plan(entities, startCell, direction, width, height)
|
||||
local entityAt, inBounds = world(entities, width, height)
|
||||
return Movement.findMovableGroup({startCell}, direction, entityAt, inBounds)
|
||||
end
|
||||
|
||||
test("ordinary pull only drags the block touching the player", function()
|
||||
-- regular, regular, player; moving right
|
||||
local rear = movable(2, 3)
|
||||
local touching = movable(3, 3)
|
||||
local possible, group = plan({rear, touching}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, touching)
|
||||
assertNotIn(group, rear)
|
||||
end)
|
||||
|
||||
test("a trailing sticky block joins a horizontal pull", function()
|
||||
-- sticky, regular, player; moving right
|
||||
local sticky = movable(2, 3, true)
|
||||
local regular = movable(3, 3)
|
||||
local possible, group = plan({sticky, regular}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertIn(group, sticky)
|
||||
end)
|
||||
|
||||
test("a sticky block touching the player pulls a regular block behind it", function()
|
||||
-- regular, sticky, player; moving right
|
||||
local regular = movable(2, 3)
|
||||
local sticky = movable(3, 3, true)
|
||||
local possible, group = plan({regular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, sticky)
|
||||
assertIn(group, regular)
|
||||
end)
|
||||
|
||||
test("sticky pull chains work vertically", function()
|
||||
-- player moves down, pulling the two blocks above it
|
||||
local sticky = movable(4, 2, true)
|
||||
local regular = movable(4, 3)
|
||||
local possible, group = plan({sticky, regular}, {x = 4, y = 3}, {x = 0, y = 1})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertIn(group, sticky)
|
||||
end)
|
||||
|
||||
test("sticky pulls are symmetric in all four directions", function()
|
||||
for _, direction in ipairs({
|
||||
{x = 1, y = 0}, {x = -1, y = 0},
|
||||
{x = 0, y = 1}, {x = 0, y = -1},
|
||||
}) do
|
||||
local player = {x = 4, y = 4}
|
||||
local regular = movable(player.x - direction.x, player.y - direction.y)
|
||||
local sticky = movable(player.x - direction.x * 2, player.y - direction.y * 2, true)
|
||||
local possible, group = plan(
|
||||
{sticky, regular},
|
||||
{x = regular.x, y = regular.y},
|
||||
direction
|
||||
)
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertIn(group, sticky)
|
||||
end
|
||||
end)
|
||||
|
||||
test("a sticky block shears off during orthogonal movement", function()
|
||||
-- The regular block moves right; the sticky block above it stays behind.
|
||||
local regular = movable(3, 4)
|
||||
local stickyBranch = movable(3, 3, true)
|
||||
local possible, group = plan({regular, stickyBranch}, {x = 3, y = 4}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertNotIn(group, stickyBranch)
|
||||
end)
|
||||
|
||||
test("a blocked sheared sticky block does not stop movement", function()
|
||||
local regular = movable(3, 4)
|
||||
local stickyBranch = movable(3, 3, true)
|
||||
local blockingWall = wall(4, 3)
|
||||
local possible, group = plan({regular, stickyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertNotIn(group, stickyBranch)
|
||||
end)
|
||||
|
||||
test("a skewer carries an orthogonal branch", function()
|
||||
local regular = movable(3, 4)
|
||||
local skewerBranch = movable(3, 3, false, nil, true)
|
||||
local possible, group = plan({regular, skewerBranch}, {x = 3, y = 4}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertIn(group, skewerBranch)
|
||||
end)
|
||||
|
||||
test("a blocked skewer branch stops its rigid movement group", function()
|
||||
local regular = movable(3, 4)
|
||||
local skewerBranch = movable(3, 3, false, nil, true)
|
||||
local blockingWall = wall(4, 3)
|
||||
local possible = plan({regular, skewerBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
|
||||
assertFalse(possible)
|
||||
end)
|
||||
|
||||
test("ordinary pushing still propagates through regular blocks", function()
|
||||
local first = movable(3, 3)
|
||||
local second = movable(4, 3)
|
||||
local possible, group = plan({first, second}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, first)
|
||||
assertIn(group, second)
|
||||
end)
|
||||
|
||||
test("diagonal blocks do not stick", function()
|
||||
local regular = movable(3, 3)
|
||||
local diagonalSticky = movable(2, 2, true)
|
||||
local possible, group = plan({regular, diagonalSticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, regular)
|
||||
assertNotIn(group, diagonalSticky)
|
||||
end)
|
||||
|
||||
test("multi-cell sticky blocks pull from every trailing occupied cell", function()
|
||||
local tallRegular = movable(3, 3, false, {{x = 0, y = 0}, {x = 0, y = 1}})
|
||||
local sticky = movable(2, 4, true)
|
||||
local possible, group = plan({tallRegular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
|
||||
assertTrue(possible)
|
||||
assertIn(group, tallRegular)
|
||||
assertIn(group, sticky)
|
||||
end)
|
||||
|
||||
test("production sticky and skewer blocks advertise distinct behavior", function()
|
||||
class = require "libs/middleclass"
|
||||
Color = class("TestColor")
|
||||
function Color:initialize(name) self.name = name end
|
||||
function Color:getname() return self.name end
|
||||
|
||||
require "generic_moveable"
|
||||
require "generic_sticky"
|
||||
require "generic_skewer"
|
||||
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
||||
local regular = GenericMoveable:new(2, 2, "red", sprite, "box")
|
||||
local sticky = GenericSticky:new(3, 2, "red", sprite, "sticky_box")
|
||||
local skewer = GenericSkewer:new(4, 2, "red", sprite, "skewer_box")
|
||||
|
||||
assertTrue(regular:canMove())
|
||||
assertFalse(regular:isSticky())
|
||||
assertTrue(sticky:canMove())
|
||||
assertTrue(sticky:isSticky())
|
||||
assertFalse(sticky:isSkewer())
|
||||
assertTrue(skewer:canMove())
|
||||
assertFalse(skewer:isSticky())
|
||||
assertTrue(skewer:isSkewer())
|
||||
end)
|
||||
|
||||
local function loadRoomClass()
|
||||
love = {
|
||||
audio = {
|
||||
newSource = function()
|
||||
return {stop = function() end, play = function() end}
|
||||
end,
|
||||
},
|
||||
}
|
||||
require "room"
|
||||
end
|
||||
|
||||
local function roomWith(player, entities, width, height)
|
||||
gridWidth, gridHeight = width or 8, height or 8
|
||||
local matrix = {}
|
||||
for y = 1, gridHeight do
|
||||
matrix[y] = {}
|
||||
for x = 1, gridWidth do matrix[y][x] = false end
|
||||
end
|
||||
for _, entity in ipairs(entities) do
|
||||
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||
matrix[cell.y][cell.x] = entity
|
||||
end
|
||||
end
|
||||
|
||||
local room = setmetatable({
|
||||
activeColors = {red = true},
|
||||
colorsPlayerHas = {"red"},
|
||||
player = {red = player},
|
||||
collidableMatrices = {red = matrix},
|
||||
}, {__index = Room})
|
||||
room.playerJoinCheck = function() end
|
||||
room.tick = function() end
|
||||
return room
|
||||
end
|
||||
|
||||
test("Room applies the sticky-regular-player pull", function()
|
||||
loadRoomClass()
|
||||
local sticky = movable(2, 3, true)
|
||||
local regular = movable(3, 3)
|
||||
local player = movable(4, 3)
|
||||
local room = roomWith(player, {sticky, regular})
|
||||
|
||||
room:movePlayer("right")
|
||||
|
||||
assertTrue(sticky.x == 3, "sticky block should move one cell right")
|
||||
assertTrue(regular.x == 4, "regular block should move one cell right")
|
||||
assertTrue(player.x == 5, "player should move one cell right")
|
||||
end)
|
||||
|
||||
test("Room shears a lateral sticky contact", function()
|
||||
loadRoomClass()
|
||||
local sticky = movable(3, 2, true)
|
||||
local regular = movable(3, 3)
|
||||
local player = movable(4, 3)
|
||||
local room = roomWith(player, {sticky, regular})
|
||||
|
||||
room:movePlayer("right")
|
||||
|
||||
assertTrue(sticky.x == 3, "lateral sticky block should shear and stay behind")
|
||||
assertTrue(regular.x == 4)
|
||||
assertTrue(player.x == 5)
|
||||
end)
|
||||
|
||||
test("Room never moves a component found by push and pull twice", function()
|
||||
loadRoomClass()
|
||||
local player = movable(4, 4)
|
||||
local front = movable(5, 4)
|
||||
local bridgeRight = movable(5, 3, false, nil, true)
|
||||
local bridgeMiddle = movable(4, 3, false, nil, true)
|
||||
local bridgeLeft = movable(3, 3, false, nil, true)
|
||||
local rear = movable(3, 4)
|
||||
local component = {front, bridgeRight, bridgeMiddle, bridgeLeft, rear}
|
||||
local room = roomWith(player, component)
|
||||
|
||||
room:movePlayer("right")
|
||||
|
||||
for _, entity in ipairs(component) do
|
||||
assertTrue(entity.x <= 6, "an entity in the shared component moved more than once")
|
||||
end
|
||||
assertTrue(front.x == 6)
|
||||
assertTrue(bridgeRight.x == 6)
|
||||
assertTrue(bridgeMiddle.x == 5)
|
||||
assertTrue(bridgeLeft.x == 4)
|
||||
assertTrue(rear.x == 4)
|
||||
assertTrue(player.x == 5)
|
||||
end)
|
||||
|
||||
test("every moveable base box gets a synthetic sticky variant", function()
|
||||
local BoxVariants = require "box_variants"
|
||||
local boxSprites = {red = "box_r.png"}
|
||||
local boxCells = {red = {{x = 1, y = 1}}}
|
||||
local shapeSprites = {blue = "box_shape_b.png"}
|
||||
local shapeCells = {blue = {{x = 1, y = 1}, {x = 2, y = 1}}}
|
||||
local assets = {
|
||||
box = {class = "moveable", sprites = boxSprites, cells = boxCells},
|
||||
["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells},
|
||||
box_statue = {class = "immoveable", sprites = {}},
|
||||
beer_mug = {class = "moveable", sprites = {}},
|
||||
}
|
||||
|
||||
BoxVariants.addTo(assets)
|
||||
|
||||
assertTrue(assets.sticky_box.class == "sticky")
|
||||
assertTrue(assets.sticky_box.directory == "sticky")
|
||||
assertTrue(assets.sticky_box.synthetic)
|
||||
assertTrue(assets.sticky_box.goopy)
|
||||
assertTrue(assets.sticky_box.sprites == boxSprites, "variant should share base sprites")
|
||||
assertTrue(assets.sticky_box.cells == boxCells, "variant should share collision cells")
|
||||
assertTrue(assets["sticky_box_2x1_11"].sprites == shapeSprites)
|
||||
assertTrue(assets["sticky_box_2x1_11"].cells == shapeCells)
|
||||
assertTrue(assets.skewer_box.class == "skewer")
|
||||
assertTrue(assets.skewer_box.directory == "skewer")
|
||||
assertTrue(assets.skewer_box.synthetic)
|
||||
assertTrue(assets.skewer_box.skewer)
|
||||
assertTrue(assets.skewer_box.sprites == boxSprites)
|
||||
assertTrue(assets.skewer_box.cells == boxCells)
|
||||
assertTrue(assets["skewer_box_2x1_11"].sprites == shapeSprites)
|
||||
assertFalse(assets.sticky_box_statue)
|
||||
assertFalse(assets.sticky_beer_mug)
|
||||
assertFalse(assets.skewer_box_statue)
|
||||
assertFalse(assets.skewer_beer_mug)
|
||||
end)
|
||||
|
||||
print("movement/block-variant tests passed: " .. passed)
|
||||
Loading…
Add table
Add a link
Reference in a new issue