Add spiky box variants
This commit is contained in:
parent
761214fe3e
commit
c3a2692351
10 changed files with 340 additions and 19 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
local BoxVariants = {}
|
local BoxVariants = {}
|
||||||
|
|
||||||
-- Add directional sticky-puller and all-edge skewer variants for every authored
|
-- Add directional sticky-puller, rigid skewer, and soft all-edge spiky variants
|
||||||
-- moveable box. Both share sprite paths and collision cells with their base.
|
-- for every authored moveable box. All share sprites/collision with their base.
|
||||||
function BoxVariants.addTo(assets)
|
function BoxVariants.addTo(assets)
|
||||||
local baseBoxes = {}
|
local baseBoxes = {}
|
||||||
for name, properties in pairs(assets) do
|
for name, properties in pairs(assets) do
|
||||||
|
|
@ -28,6 +28,7 @@ function BoxVariants.addTo(assets)
|
||||||
for _, base in ipairs(baseBoxes) do
|
for _, base in ipairs(baseBoxes) do
|
||||||
addVariant(base, "sticky_", "sticky", "sticky", "goopy")
|
addVariant(base, "sticky_", "sticky", "sticky", "goopy")
|
||||||
addVariant(base, "skewer_", "skewer", "skewer", "skewer")
|
addVariant(base, "skewer_", "skewer", "skewer", "skewer")
|
||||||
|
addVariant(base, "spiky_", "spiky", "spiky", "spiky")
|
||||||
end
|
end
|
||||||
|
|
||||||
return assets
|
return assets
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ function Entity:initialize(t)
|
||||||
self.collision = t.collision --"none", "moveable", "unmoveable"
|
self.collision = t.collision --"none", "moveable", "unmoveable"
|
||||||
self.sticky = t.sticky or false
|
self.sticky = t.sticky or false
|
||||||
self.skewer = t.skewer or false
|
self.skewer = t.skewer or false
|
||||||
|
self.spiky = t.spiky or false
|
||||||
self.sprite = repSprite
|
self.sprite = repSprite
|
||||||
self.shaAmount = t.sha or 1
|
self.shaAmount = t.sha or 1
|
||||||
-- onBump is a method (see Entity:onBump) so mixins like Dialogable can
|
-- onBump is a method (see Entity:onBump) so mixins like Dialogable can
|
||||||
|
|
@ -144,6 +145,10 @@ function Entity:isSkewer()
|
||||||
return self.skewer
|
return self.skewer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Entity:isSpiky()
|
||||||
|
return self.spiky
|
||||||
|
end
|
||||||
|
|
||||||
function Entity:canPassOver()
|
function Entity:canPassOver()
|
||||||
if self:getCollision() == "none" then
|
if self:getCollision() == "none" then
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ end
|
||||||
|
|
||||||
-- Draw two teeth on every exposed cell edge. The art remains synthetic: the
|
-- Draw two teeth on every exposed cell edge. The art remains synthetic: the
|
||||||
-- underlying box sprite and its collision shape are still the authored base.
|
-- underlying box sprite and its collision shape are still the authored base.
|
||||||
function drawSkewerSprite(sprite, cellShape, x, y, scale)
|
function drawSpikes(cellShape, x, y, scale)
|
||||||
cellShape = cellShape or {{x = 1, y = 1}}
|
cellShape = cellShape or {{x = 1, y = 1}}
|
||||||
local cellSize = 16 * scale
|
local cellSize = 16 * scale
|
||||||
local inset = scale
|
local inset = scale
|
||||||
|
|
@ -50,7 +50,10 @@ function drawSkewerSprite(sprite, cellShape, x, y, scale)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function drawSkewerSprite(sprite, cellShape, x, y, scale)
|
||||||
|
drawSpikes(cellShape, x, y, scale)
|
||||||
love.graphics.draw(sprite, x, y, 0, scale, scale)
|
love.graphics.draw(sprite, x, y, 0, scale, scale)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
46
generic_spiky.lua
Normal file
46
generic_spiky.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
require "entity"
|
||||||
|
require "generic_skewer"
|
||||||
|
|
||||||
|
GenericSpiky = class("GenericSpiky", Entity)
|
||||||
|
|
||||||
|
function drawSpikySprite(sprite, _, x, y, scale)
|
||||||
|
if drawGoopySprite then
|
||||||
|
drawGoopySprite(sprite, x, y, scale)
|
||||||
|
else
|
||||||
|
love.graphics.draw(sprite, x, y, 0, scale, scale)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSpiky:initialize(x, y, color, sprite, name, cellShape)
|
||||||
|
Entity.initialize(self, {
|
||||||
|
name = name,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
color = color,
|
||||||
|
collision = "moveable",
|
||||||
|
spiky = true,
|
||||||
|
sprite = sprite,
|
||||||
|
cellShape = cellShape
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSpiky:update()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSpiky:draw(channel)
|
||||||
|
local sprite = self.sprites[channel]
|
||||||
|
if not sprite then return end
|
||||||
|
love.graphics.setColor(gColor[channel]:set())
|
||||||
|
local p = self:getDrawPos()
|
||||||
|
drawSpikySprite(
|
||||||
|
sprite, self:getCellShape(),
|
||||||
|
p.x + self.drawOffset.x * drawScale,
|
||||||
|
p.y + self.drawOffset.y * drawScale,
|
||||||
|
drawScale
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSpiky:tick()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
@ -364,6 +364,7 @@ local function rebuildAssetSearchButtons()
|
||||||
sprites = sprites,
|
sprites = sprites,
|
||||||
goopy = gameAssets[asset].goopy,
|
goopy = gameAssets[asset].goopy,
|
||||||
skewer = gameAssets[asset].skewer,
|
skewer = gameAssets[asset].skewer,
|
||||||
|
spiky = gameAssets[asset].spiky,
|
||||||
cells = gameAssets[asset].cells,
|
cells = gameAssets[asset].cells,
|
||||||
name = "asset",
|
name = "asset",
|
||||||
text = asset,
|
text = asset,
|
||||||
|
|
@ -685,6 +686,7 @@ function selectFolder(folder)
|
||||||
sprites = sprites,
|
sprites = sprites,
|
||||||
goopy = gameAssets[asset].goopy,
|
goopy = gameAssets[asset].goopy,
|
||||||
skewer = gameAssets[asset].skewer,
|
skewer = gameAssets[asset].skewer,
|
||||||
|
spiky = gameAssets[asset].spiky,
|
||||||
cells = gameAssets[asset].cells,
|
cells = gameAssets[asset].cells,
|
||||||
name = "asset",
|
name = "asset",
|
||||||
text = asset,
|
text = asset,
|
||||||
|
|
@ -759,6 +761,7 @@ function editAsset(asset)
|
||||||
sprites = assetSprites,
|
sprites = assetSprites,
|
||||||
goopy = assetProperties.goopy,
|
goopy = assetProperties.goopy,
|
||||||
skewer = assetProperties.skewer,
|
skewer = assetProperties.skewer,
|
||||||
|
spiky = assetProperties.spiky,
|
||||||
cells = assetProperties.cells,
|
cells = assetProperties.cells,
|
||||||
x = sb.w / 2 - 18,
|
x = sb.w / 2 - 18,
|
||||||
y = 80,
|
y = 80,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ function GUI:initialize(t)
|
||||||
self.text = t.text
|
self.text = t.text
|
||||||
self.goopy = t.goopy or false
|
self.goopy = t.goopy or false
|
||||||
self.skewer = t.skewer or false
|
self.skewer = t.skewer or false
|
||||||
|
self.spiky = t.spiky or false
|
||||||
self.cells = t.cells
|
self.cells = t.cells
|
||||||
|
|
||||||
if not t.onClic then
|
if not t.onClic then
|
||||||
|
|
@ -106,6 +107,8 @@ function GUI:draw()
|
||||||
drawGoopySprite(sprite, box.x, box.y, self.scale)
|
drawGoopySprite(sprite, box.x, box.y, self.scale)
|
||||||
elseif self.skewer and drawSkewerSprite then
|
elseif self.skewer and drawSkewerSprite then
|
||||||
drawSkewerSprite(sprite, self.cells and self.cells[color], box.x, box.y, self.scale)
|
drawSkewerSprite(sprite, self.cells and self.cells[color], box.x, box.y, self.scale)
|
||||||
|
elseif self.spiky and drawSpikySprite then
|
||||||
|
drawSpikySprite(sprite, self.cells and self.cells[color], box.x, box.y, self.scale)
|
||||||
else
|
else
|
||||||
love.graphics.draw(sprite, box.x, box.y, 0, self.scale, self.scale)
|
love.graphics.draw(sprite, box.x, box.y, 0, self.scale, self.scale)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
82
movement.lua
82
movement.lua
|
|
@ -1,6 +1,6 @@
|
||||||
-- Finds the complete set of blocks that must move together for one grid step.
|
-- Finds the complete set of blocks that must move together for one grid step.
|
||||||
-- A block joins the set when it is in another block's destination (ordinary
|
-- A block joins the set when it is in another block's destination (ordinary
|
||||||
-- pushing), is behind a directional sticky puller, or touches a skewer block.
|
-- pushing), is softly grabbed by sticky/spiky, or touches a rigid skewer.
|
||||||
local Movement = {}
|
local Movement = {}
|
||||||
|
|
||||||
local adjacentDirections = {
|
local adjacentDirections = {
|
||||||
|
|
@ -22,7 +22,14 @@ local function isSkewer(entity)
|
||||||
return entity.isSkewer and entity:isSkewer()
|
return entity.isSkewer and entity:isSkewer()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
local function isSpiky(entity)
|
||||||
|
return entity.isSpiky and entity:isSpiky()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build one rigid component: ordinary pushes are required, and skewers join
|
||||||
|
-- along every edge. `alreadyMoving` contains cells that will be vacated during
|
||||||
|
-- this same step, so a later pulled component may move into them.
|
||||||
|
local function findRigidGroup(startCells, direction, entityAt, isInBounds, alreadyMoving)
|
||||||
local entities = {}
|
local entities = {}
|
||||||
local pendingCells = {}
|
local pendingCells = {}
|
||||||
for _, cell in ipairs(startCells) do
|
for _, cell in ipairs(startCells) do
|
||||||
|
|
@ -39,7 +46,8 @@ function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
end
|
end
|
||||||
|
|
||||||
local entity = entityAt(cell)
|
local entity = entityAt(cell)
|
||||||
if entity and not isPassable(entity) and not entities[entity] then
|
if entity and not isPassable(entity) and not entities[entity] and
|
||||||
|
not (alreadyMoving and alreadyMoving[entity]) then
|
||||||
if not entity:canMove() then
|
if not entity:canMove() then
|
||||||
return false, entities
|
return false, entities
|
||||||
end
|
end
|
||||||
|
|
@ -51,9 +59,7 @@ function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
table.insert(pendingCells, projectedCell)
|
table.insert(pendingCells, projectedCell)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sticky blocks pull only from the trailing face, like the player. A
|
-- Skewers make every edge contact part of this rigid component.
|
||||||
-- lateral move therefore shears the contact. Skewers retain the older
|
|
||||||
-- all-edge behaviour and make everything they touch one rigid group.
|
|
||||||
for _, occupiedCell in ipairs(entity:getOccupiedCells()) do
|
for _, occupiedCell in ipairs(entity:getOccupiedCells()) do
|
||||||
for _, adjacentDirection in ipairs(adjacentDirections) do
|
for _, adjacentDirection in ipairs(adjacentDirections) do
|
||||||
local adjacentCell = {
|
local adjacentCell = {
|
||||||
|
|
@ -62,13 +68,11 @@ function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
}
|
}
|
||||||
if isInBounds(adjacentCell) then
|
if isInBounds(adjacentCell) then
|
||||||
local adjacentEntity = entityAt(adjacentCell)
|
local adjacentEntity = entityAt(adjacentCell)
|
||||||
local trailing = adjacentDirection.x == -direction.x and
|
|
||||||
adjacentDirection.y == -direction.y
|
|
||||||
local attached = adjacentEntity and (
|
local attached = adjacentEntity and (
|
||||||
isSkewer(entity) or isSkewer(adjacentEntity) or
|
isSkewer(entity) or isSkewer(adjacentEntity)
|
||||||
(trailing and (isSticky(entity) or isSticky(adjacentEntity)))
|
|
||||||
)
|
)
|
||||||
if attached and adjacentEntity ~= entity and adjacentEntity:canMove() then
|
if attached and adjacentEntity ~= entity and adjacentEntity:canMove() and
|
||||||
|
not (alreadyMoving and alreadyMoving[adjacentEntity]) then
|
||||||
table.insert(pendingCells, adjacentCell)
|
table.insert(pendingCells, adjacentCell)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -80,4 +84,60 @@ function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
return true, entities
|
return true, entities
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
|
local possible, entities = findRigidGroup(
|
||||||
|
startCells, direction, entityAt, isInBounds
|
||||||
|
)
|
||||||
|
if not possible then return false, entities end
|
||||||
|
|
||||||
|
local movingQueue = {}
|
||||||
|
for entity in pairs(entities) do table.insert(movingQueue, entity) end
|
||||||
|
local attemptedPulls = {}
|
||||||
|
local nextMover = 1
|
||||||
|
|
||||||
|
while nextMover <= #movingQueue do
|
||||||
|
local entity = movingQueue[nextMover]
|
||||||
|
nextMover = nextMover + 1
|
||||||
|
|
||||||
|
-- Sticky contact is soft only on the trailing face. Spiky contact is soft
|
||||||
|
-- on every face. Either kind simply releases a blocked rigid component,
|
||||||
|
-- without cancelling this entity or another independently pulled branch.
|
||||||
|
for _, occupiedCell in ipairs(entity:getOccupiedCells()) do
|
||||||
|
for _, adjacentDirection in ipairs(adjacentDirections) do
|
||||||
|
local adjacentCell = {
|
||||||
|
x = occupiedCell.x + adjacentDirection.x,
|
||||||
|
y = occupiedCell.y + adjacentDirection.y,
|
||||||
|
}
|
||||||
|
if isInBounds(adjacentCell) then
|
||||||
|
local adjacentEntity = entityAt(adjacentCell)
|
||||||
|
local trailing = adjacentDirection.x == -direction.x and
|
||||||
|
adjacentDirection.y == -direction.y
|
||||||
|
local softContact = adjacentEntity and (
|
||||||
|
isSpiky(entity) or isSpiky(adjacentEntity) or
|
||||||
|
(trailing and (isSticky(entity) or isSticky(adjacentEntity)))
|
||||||
|
)
|
||||||
|
if softContact and adjacentEntity ~= entity and
|
||||||
|
not entities[adjacentEntity] and not attemptedPulls[adjacentEntity] and
|
||||||
|
adjacentEntity:canMove() then
|
||||||
|
attemptedPulls[adjacentEntity] = true
|
||||||
|
local canPull, pulled = findRigidGroup(
|
||||||
|
{adjacentCell}, direction, entityAt, isInBounds, entities
|
||||||
|
)
|
||||||
|
if canPull then
|
||||||
|
for pulledEntity in pairs(pulled) do
|
||||||
|
if not entities[pulledEntity] then
|
||||||
|
entities[pulledEntity] = pulledEntity
|
||||||
|
table.insert(movingQueue, pulledEntity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, entities
|
||||||
|
end
|
||||||
|
|
||||||
return Movement
|
return Movement
|
||||||
|
|
|
||||||
5
room.lua
5
room.lua
|
|
@ -12,6 +12,7 @@ require "dialogue_manager"
|
||||||
require "generic_moveable"
|
require "generic_moveable"
|
||||||
require "generic_sticky"
|
require "generic_sticky"
|
||||||
require "generic_skewer"
|
require "generic_skewer"
|
||||||
|
require "generic_spiky"
|
||||||
require "generic_unmoveable"
|
require "generic_unmoveable"
|
||||||
|
|
||||||
local Movement = require "movement"
|
local Movement = require "movement"
|
||||||
|
|
@ -465,7 +466,7 @@ end
|
||||||
|
|
||||||
-- Instantiate one object from the flat {class,color,x,y} schema. `class` names an
|
-- Instantiate one object from the flat {class,color,x,y} schema. `class` names an
|
||||||
-- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/
|
-- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/
|
||||||
-- "moveable"/"sticky"/"skewer"); anything without a .meta defaults to immoveable. npc is the one
|
-- "moveable"/"sticky"/"skewer"/"spiky"); anything without a .meta defaults to immoveable. npc is the one
|
||||||
-- keyword handled directly since it has no art.
|
-- keyword handled directly since it has no art.
|
||||||
function Room:createEntity(class, color, x, y)
|
function Room:createEntity(class, color, x, y)
|
||||||
if class == "npc" then
|
if class == "npc" then
|
||||||
|
|
@ -509,6 +510,8 @@ function Room:createEntity(class, color, x, y)
|
||||||
entity = GenericSticky:new(x, y, color, sprite, class, cellShape)
|
entity = GenericSticky:new(x, y, color, sprite, class, cellShape)
|
||||||
elseif behavior == "skewer" then
|
elseif behavior == "skewer" then
|
||||||
entity = GenericSkewer:new(x, y, color, sprite, class, cellShape)
|
entity = GenericSkewer:new(x, y, color, sprite, class, cellShape)
|
||||||
|
elseif behavior == "spiky" then
|
||||||
|
entity = GenericSpiky:new(x, y, color, sprite, class, cellShape)
|
||||||
else
|
else
|
||||||
entity = GenericUnmoveable:new(x, y, color, sprite, cellShape)
|
entity = GenericUnmoveable:new(x, y, color, sprite, cellShape)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,19 @@ local function assertNotIn(group, entity, message)
|
||||||
if group[entity] then error(message or "expected entity outside movement group", 2) end
|
if group[entity] then error(message or "expected entity outside movement group", 2) end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function movable(x, y, sticky, shape, skewer)
|
local function movable(x, y, sticky, shape, skewer, spiky)
|
||||||
local entity = {
|
local entity = {
|
||||||
x = x, y = y,
|
x = x, y = y,
|
||||||
sticky = sticky or false,
|
sticky = sticky or false,
|
||||||
skewer = skewer or false,
|
skewer = skewer or false,
|
||||||
|
spiky = spiky or false,
|
||||||
shape = shape or {{x = 0, y = 0}},
|
shape = shape or {{x = 0, y = 0}},
|
||||||
}
|
}
|
||||||
function entity:canMove() return true end
|
function entity:canMove() return true end
|
||||||
function entity:canPassOver() return false end
|
function entity:canPassOver() return false end
|
||||||
function entity:isSticky() return self.sticky end
|
function entity:isSticky() return self.sticky end
|
||||||
function entity:isSkewer() return self.skewer end
|
function entity:isSkewer() return self.skewer end
|
||||||
|
function entity:isSpiky() return self.spiky end
|
||||||
function entity:bump() end
|
function entity:bump() end
|
||||||
function entity:move(axis, amount) self[axis] = self[axis] + amount 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:getGridPos() return {x = self.x, y = self.y} end
|
||||||
|
|
@ -100,7 +102,7 @@ test("a trailing sticky block joins a horizontal pull", function()
|
||||||
assertIn(group, sticky)
|
assertIn(group, sticky)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("a sticky block touching the player pulls a regular block behind it", function()
|
test("a moving sticky block pulls a regular block behind it", function()
|
||||||
-- regular, sticky, player; moving right
|
-- regular, sticky, player; moving right
|
||||||
local regular = movable(2, 3)
|
local regular = movable(2, 3)
|
||||||
local sticky = movable(3, 3, true)
|
local sticky = movable(3, 3, true)
|
||||||
|
|
@ -176,6 +178,36 @@ test("a blocked skewer branch stops its rigid movement group", function()
|
||||||
assertFalse(possible)
|
assertFalse(possible)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("a spiky block pulls both orthogonal side blocks", function()
|
||||||
|
local spiky = movable(4, 4, false, nil, false, true)
|
||||||
|
local upper = movable(4, 3)
|
||||||
|
local lower = movable(4, 5)
|
||||||
|
local possible, group = plan(
|
||||||
|
{spiky, upper, lower}, {x = 4, y = 4}, {x = 1, y = 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, spiky)
|
||||||
|
assertIn(group, upper)
|
||||||
|
assertIn(group, lower)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("a spiky block releases one blocked side while pulling the other", function()
|
||||||
|
local spiky = movable(4, 4, false, nil, false, true)
|
||||||
|
local blockedUpper = movable(4, 3)
|
||||||
|
local freeLower = movable(4, 5)
|
||||||
|
local blockingWall = wall(5, 3)
|
||||||
|
local possible, group = plan(
|
||||||
|
{spiky, blockedUpper, freeLower, blockingWall},
|
||||||
|
{x = 4, y = 4}, {x = 1, y = 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, spiky)
|
||||||
|
assertNotIn(group, blockedUpper)
|
||||||
|
assertIn(group, freeLower)
|
||||||
|
end)
|
||||||
|
|
||||||
test("ordinary pushing still propagates through regular blocks", function()
|
test("ordinary pushing still propagates through regular blocks", function()
|
||||||
local first = movable(3, 3)
|
local first = movable(3, 3)
|
||||||
local second = movable(4, 3)
|
local second = movable(4, 3)
|
||||||
|
|
@ -203,7 +235,88 @@ test("multi-cell sticky blocks pull from every trailing occupied cell", function
|
||||||
assertIn(group, sticky)
|
assertIn(group, sticky)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("production sticky and skewer blocks advertise distinct behavior", function()
|
test("an L-shaped sticky pulls from its trailing face in every direction", function()
|
||||||
|
local lShape = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1}}
|
||||||
|
local cases = {
|
||||||
|
{direction = {x = 1, y = 0}, trailing = {x = 2, y = 3}},
|
||||||
|
{direction = {x = -1, y = 0}, trailing = {x = 5, y = 3}},
|
||||||
|
{direction = {x = 0, y = 1}, trailing = {x = 3, y = 2}},
|
||||||
|
{direction = {x = 0, y = -1}, trailing = {x = 3, y = 5}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, case in ipairs(cases) do
|
||||||
|
local stickyL = movable(3, 3, true, lShape)
|
||||||
|
local trailing = movable(case.trailing.x, case.trailing.y)
|
||||||
|
local possible, group = plan(
|
||||||
|
{stickyL, trailing}, {x = 3, y = 3}, case.direction
|
||||||
|
)
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, stickyL)
|
||||||
|
assertIn(group, trailing)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("an L-shaped sticky can release one blocked pull while completing another", function()
|
||||||
|
-- Moving right, the two exposed trailing cells touch separate objects. The
|
||||||
|
-- upper object is pinned against a wall; the lower one is free to follow.
|
||||||
|
local stickyL = movable(3, 3, true, {
|
||||||
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
|
||||||
|
})
|
||||||
|
local pinned = movable(2, 2, false, {{x = 0, y = 0}, {x = 0, y = 1}})
|
||||||
|
local free = movable(2, 4)
|
||||||
|
local blockingWall = wall(3, 2)
|
||||||
|
local possible, group = plan(
|
||||||
|
{stickyL, pinned, free, blockingWall},
|
||||||
|
{x = 3, y = 3}, {x = 1, y = 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, stickyL)
|
||||||
|
assertIn(group, free)
|
||||||
|
assertNotIn(group, pinned)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("a T-shaped sticky pulls independently across a notched trailing face", function()
|
||||||
|
-- Moving up, three exposed lower faces pull into cells vacated by the T.
|
||||||
|
local stickyT = movable(3, 3, true, {
|
||||||
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 2, y = 0},
|
||||||
|
{x = 1, y = 1},
|
||||||
|
})
|
||||||
|
local trailingLeft = movable(3, 4)
|
||||||
|
local trailingRight = movable(5, 4)
|
||||||
|
local trailingStem = movable(4, 5)
|
||||||
|
local possible, group = plan(
|
||||||
|
{stickyT, trailingLeft, trailingRight, trailingStem},
|
||||||
|
{x = 3, y = 3}, {x = 0, y = -1}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, stickyT)
|
||||||
|
assertIn(group, trailingLeft)
|
||||||
|
assertIn(group, trailingRight)
|
||||||
|
assertIn(group, trailingStem)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("an L-shaped spiky block grabs moveables across several exposed faces", function()
|
||||||
|
local spikyL = movable(3, 3, false, {
|
||||||
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
|
||||||
|
}, false, true)
|
||||||
|
local above = movable(3, 2)
|
||||||
|
local below = movable(3, 5)
|
||||||
|
local behind = movable(2, 3)
|
||||||
|
local possible, group = plan(
|
||||||
|
{spikyL, above, below, behind},
|
||||||
|
{x = 3, y = 3}, {x = 1, y = 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(possible)
|
||||||
|
assertIn(group, spikyL)
|
||||||
|
assertIn(group, above)
|
||||||
|
assertIn(group, below)
|
||||||
|
assertIn(group, behind)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("production sticky, skewer, and spiky blocks advertise distinct behavior", function()
|
||||||
class = require "libs/middleclass"
|
class = require "libs/middleclass"
|
||||||
Color = class("TestColor")
|
Color = class("TestColor")
|
||||||
function Color:initialize(name) self.name = name end
|
function Color:initialize(name) self.name = name end
|
||||||
|
|
@ -212,10 +325,12 @@ test("production sticky and skewer blocks advertise distinct behavior", function
|
||||||
require "generic_moveable"
|
require "generic_moveable"
|
||||||
require "generic_sticky"
|
require "generic_sticky"
|
||||||
require "generic_skewer"
|
require "generic_skewer"
|
||||||
|
require "generic_spiky"
|
||||||
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
|
||||||
local regular = GenericMoveable:new(2, 2, "red", sprite, "box")
|
local regular = GenericMoveable:new(2, 2, "red", sprite, "box")
|
||||||
local sticky = GenericSticky:new(3, 2, "red", sprite, "sticky_box")
|
local sticky = GenericSticky:new(3, 2, "red", sprite, "sticky_box")
|
||||||
local skewer = GenericSkewer:new(4, 2, "red", sprite, "skewer_box")
|
local skewer = GenericSkewer:new(4, 2, "red", sprite, "skewer_box")
|
||||||
|
local spiky = GenericSpiky:new(5, 2, "red", sprite, "spiky_box")
|
||||||
|
|
||||||
assertTrue(regular:canMove())
|
assertTrue(regular:canMove())
|
||||||
assertFalse(regular:isSticky())
|
assertFalse(regular:isSticky())
|
||||||
|
|
@ -225,6 +340,11 @@ test("production sticky and skewer blocks advertise distinct behavior", function
|
||||||
assertTrue(skewer:canMove())
|
assertTrue(skewer:canMove())
|
||||||
assertFalse(skewer:isSticky())
|
assertFalse(skewer:isSticky())
|
||||||
assertTrue(skewer:isSkewer())
|
assertTrue(skewer:isSkewer())
|
||||||
|
assertFalse(skewer:isSpiky())
|
||||||
|
assertTrue(spiky:canMove())
|
||||||
|
assertFalse(spiky:isSticky())
|
||||||
|
assertFalse(spiky:isSkewer())
|
||||||
|
assertTrue(spiky:isSpiky())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function loadRoomClass()
|
local function loadRoomClass()
|
||||||
|
|
@ -276,6 +396,47 @@ test("Room applies the sticky-regular-player pull", function()
|
||||||
assertTrue(player.x == 5, "player should move one cell right")
|
assertTrue(player.x == 5, "player should move one cell right")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("Room pulls sticky out of a pinned concave moveable", function()
|
||||||
|
loadRoomClass()
|
||||||
|
-- Before: ___ After: p__
|
||||||
|
-- px_ sx_
|
||||||
|
-- sm_ _m_
|
||||||
|
-- mm_ mm_
|
||||||
|
local player = movable(1, 2)
|
||||||
|
local wall = wall(2, 2)
|
||||||
|
local sticky = movable(1, 3, true)
|
||||||
|
local concave = movable(1, 3, false, {
|
||||||
|
{x = 1, y = 0},
|
||||||
|
{x = 0, y = 1}, {x = 1, y = 1},
|
||||||
|
})
|
||||||
|
local room = roomWith(player, {wall, sticky, concave})
|
||||||
|
|
||||||
|
room:movePlayer("up")
|
||||||
|
|
||||||
|
assertTrue(player.x == 1 and player.y == 1)
|
||||||
|
assertTrue(sticky.x == 1 and sticky.y == 2, "sticky should peel away from the concave block")
|
||||||
|
assertTrue(concave.x == 1 and concave.y == 3, "pinned concave block should stay put")
|
||||||
|
assertTrue(wall.x == 2 and wall.y == 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("Room moves an L-shaped sticky and both available trailing pulls", function()
|
||||||
|
loadRoomClass()
|
||||||
|
local player = movable(3, 2)
|
||||||
|
local stickyL = movable(3, 3, true, {
|
||||||
|
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
|
||||||
|
})
|
||||||
|
local trailingLeft = movable(3, 5)
|
||||||
|
local trailingRight = movable(4, 4)
|
||||||
|
local room = roomWith(player, {stickyL, trailingLeft, trailingRight})
|
||||||
|
|
||||||
|
room:movePlayer("up")
|
||||||
|
|
||||||
|
assertTrue(player.x == 3 and player.y == 1)
|
||||||
|
assertTrue(stickyL.x == 3 and stickyL.y == 2)
|
||||||
|
assertTrue(trailingLeft.x == 3 and trailingLeft.y == 4)
|
||||||
|
assertTrue(trailingRight.x == 4 and trailingRight.y == 3)
|
||||||
|
end)
|
||||||
|
|
||||||
test("Room shears a lateral sticky contact", function()
|
test("Room shears a lateral sticky contact", function()
|
||||||
loadRoomClass()
|
loadRoomClass()
|
||||||
local sticky = movable(3, 2, true)
|
local sticky = movable(3, 2, true)
|
||||||
|
|
@ -314,15 +475,36 @@ test("Room never moves a component found by push and pull twice", function()
|
||||||
assertTrue(player.x == 5)
|
assertTrue(player.x == 5)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("every moveable base box gets a synthetic sticky variant", function()
|
test("Room pushes spiky onward while a blocked side shears off", function()
|
||||||
|
loadRoomClass()
|
||||||
|
local player = movable(3, 4)
|
||||||
|
local spiky = movable(4, 4, false, nil, false, true)
|
||||||
|
local blockedUpper = movable(4, 3)
|
||||||
|
local freeLower = movable(4, 5)
|
||||||
|
local blockingWall = wall(5, 3)
|
||||||
|
local room = roomWith(player, {spiky, blockedUpper, freeLower, blockingWall})
|
||||||
|
|
||||||
|
room:movePlayer("right")
|
||||||
|
|
||||||
|
assertTrue(player.x == 4 and player.y == 4)
|
||||||
|
assertTrue(spiky.x == 5 and spiky.y == 4)
|
||||||
|
assertTrue(blockedUpper.x == 4 and blockedUpper.y == 3)
|
||||||
|
assertTrue(freeLower.x == 5 and freeLower.y == 5)
|
||||||
|
assertTrue(blockingWall.x == 5 and blockingWall.y == 3)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("every moveable base box gets all three synthetic behavior variants", function()
|
||||||
local BoxVariants = require "box_variants"
|
local BoxVariants = require "box_variants"
|
||||||
local boxSprites = {red = "box_r.png"}
|
local boxSprites = {red = "box_r.png"}
|
||||||
local boxCells = {red = {{x = 1, y = 1}}}
|
local boxCells = {red = {{x = 1, y = 1}}}
|
||||||
local shapeSprites = {blue = "box_shape_b.png"}
|
local shapeSprites = {blue = "box_shape_b.png"}
|
||||||
local shapeCells = {blue = {{x = 1, y = 1}, {x = 2, y = 1}}}
|
local shapeCells = {blue = {{x = 1, y = 1}, {x = 2, y = 1}}}
|
||||||
|
local lSprites = {green = "box_l_g.png"}
|
||||||
|
local lCells = {green = {{x = 1, y = 1}, {x = 1, y = 2}, {x = 2, y = 2}}}
|
||||||
local assets = {
|
local assets = {
|
||||||
box = {class = "moveable", sprites = boxSprites, cells = boxCells},
|
box = {class = "moveable", sprites = boxSprites, cells = boxCells},
|
||||||
["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells},
|
["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells},
|
||||||
|
["box_2x2_10-11"] = {class = "moveable", sprites = lSprites, cells = lCells},
|
||||||
box_statue = {class = "immoveable", sprites = {}},
|
box_statue = {class = "immoveable", sprites = {}},
|
||||||
beer_mug = {class = "moveable", sprites = {}},
|
beer_mug = {class = "moveable", sprites = {}},
|
||||||
}
|
}
|
||||||
|
|
@ -337,6 +519,8 @@ test("every moveable base box gets a synthetic sticky variant", function()
|
||||||
assertTrue(assets.sticky_box.cells == boxCells, "variant should share collision cells")
|
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"].sprites == shapeSprites)
|
||||||
assertTrue(assets["sticky_box_2x1_11"].cells == shapeCells)
|
assertTrue(assets["sticky_box_2x1_11"].cells == shapeCells)
|
||||||
|
assertTrue(assets["sticky_box_2x2_10-11"].sprites == lSprites)
|
||||||
|
assertTrue(assets["sticky_box_2x2_10-11"].cells == lCells)
|
||||||
assertTrue(assets.skewer_box.class == "skewer")
|
assertTrue(assets.skewer_box.class == "skewer")
|
||||||
assertTrue(assets.skewer_box.directory == "skewer")
|
assertTrue(assets.skewer_box.directory == "skewer")
|
||||||
assertTrue(assets.skewer_box.synthetic)
|
assertTrue(assets.skewer_box.synthetic)
|
||||||
|
|
@ -344,10 +528,21 @@ test("every moveable base box gets a synthetic sticky variant", function()
|
||||||
assertTrue(assets.skewer_box.sprites == boxSprites)
|
assertTrue(assets.skewer_box.sprites == boxSprites)
|
||||||
assertTrue(assets.skewer_box.cells == boxCells)
|
assertTrue(assets.skewer_box.cells == boxCells)
|
||||||
assertTrue(assets["skewer_box_2x1_11"].sprites == shapeSprites)
|
assertTrue(assets["skewer_box_2x1_11"].sprites == shapeSprites)
|
||||||
|
assertTrue(assets["skewer_box_2x2_10-11"].cells == lCells)
|
||||||
|
assertTrue(assets.spiky_box.class == "spiky")
|
||||||
|
assertTrue(assets.spiky_box.directory == "spiky")
|
||||||
|
assertTrue(assets.spiky_box.synthetic)
|
||||||
|
assertTrue(assets.spiky_box.spiky)
|
||||||
|
assertTrue(assets.spiky_box.sprites == boxSprites)
|
||||||
|
assertTrue(assets.spiky_box.cells == boxCells)
|
||||||
|
assertTrue(assets["spiky_box_2x1_11"].sprites == shapeSprites)
|
||||||
|
assertTrue(assets["spiky_box_2x2_10-11"].cells == lCells)
|
||||||
assertFalse(assets.sticky_box_statue)
|
assertFalse(assets.sticky_box_statue)
|
||||||
assertFalse(assets.sticky_beer_mug)
|
assertFalse(assets.sticky_beer_mug)
|
||||||
assertFalse(assets.skewer_box_statue)
|
assertFalse(assets.skewer_box_statue)
|
||||||
assertFalse(assets.skewer_beer_mug)
|
assertFalse(assets.skewer_beer_mug)
|
||||||
|
assertFalse(assets.spiky_box_statue)
|
||||||
|
assertFalse(assets.spiky_beer_mug)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print("movement/block-variant tests passed: " .. passed)
|
print("movement/block-variant tests passed: " .. passed)
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,8 @@ function World:drawRoomPreview(name, x, y, size)
|
||||||
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
||||||
elseif props.skewer and drawSkewerSprite then
|
elseif props.skewer and drawSkewerSprite then
|
||||||
drawSkewerSprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
drawSkewerSprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
||||||
|
elseif props.spiky and drawSpikySprite then
|
||||||
|
drawSpikySprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
||||||
else
|
else
|
||||||
love.graphics.draw(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
love.graphics.draw(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue