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
|
|
@ -4,12 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## What this is
|
## What this is
|
||||||
|
|
||||||
CHROMA SOLSTICE — a grid-based color-puzzle game written in Lua for the [LÖVE](https://love2d.org) framework. There is no build step, no test suite, and no package manifest; the whole game is the tree of `.lua` files loaded by `main.lua`.
|
CHROMA SOLSTICE — a grid-based color-puzzle game written in Lua for the [LÖVE](https://love2d.org) framework. There is no build step or package manifest; the whole game is the tree of `.lua` files loaded by `main.lua`.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
love . # run from the project root
|
love . # run from the project root
|
||||||
|
lua tests/test_movement.lua # run the pure matrix-movement tests
|
||||||
```
|
```
|
||||||
|
|
||||||
`love` is not installed in this environment — you cannot run the game here; reason about the code statically.
|
`love` is not installed in this environment — you cannot run the game here; reason about the code statically.
|
||||||
|
|
|
||||||
36
box_variants.lua
Normal file
36
box_variants.lua
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
local BoxVariants = {}
|
||||||
|
|
||||||
|
-- Add directional sticky-puller and all-edge skewer variants for every authored
|
||||||
|
-- moveable box. Both share sprite paths and collision cells with their base.
|
||||||
|
function BoxVariants.addTo(assets)
|
||||||
|
local baseBoxes = {}
|
||||||
|
for name, properties in pairs(assets) do
|
||||||
|
if properties.class == "moveable" and name:match("^box") then
|
||||||
|
table.insert(baseBoxes, {name = name, properties = properties})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addVariant(base, prefix, behavior, directory, visualFlag)
|
||||||
|
local name = prefix .. base.name
|
||||||
|
local properties = {
|
||||||
|
class = behavior,
|
||||||
|
directory = directory,
|
||||||
|
sprites = base.properties.sprites,
|
||||||
|
cells = base.properties.cells,
|
||||||
|
preload = base.properties.preload,
|
||||||
|
synthetic = true,
|
||||||
|
baseAsset = base.name,
|
||||||
|
}
|
||||||
|
properties[visualFlag] = true
|
||||||
|
assets[name] = properties
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, base in ipairs(baseBoxes) do
|
||||||
|
addVariant(base, "sticky_", "sticky", "sticky", "goopy")
|
||||||
|
addVariant(base, "skewer_", "skewer", "skewer", "skewer")
|
||||||
|
end
|
||||||
|
|
||||||
|
return assets
|
||||||
|
end
|
||||||
|
|
||||||
|
return BoxVariants
|
||||||
10
entity.lua
10
entity.lua
|
|
@ -14,6 +14,8 @@ function Entity:initialize(t)
|
||||||
self.name = t.name
|
self.name = t.name
|
||||||
self.color = Color:new(t.color)
|
self.color = Color:new(t.color)
|
||||||
self.collision = t.collision --"none", "moveable", "unmoveable"
|
self.collision = t.collision --"none", "moveable", "unmoveable"
|
||||||
|
self.sticky = t.sticky or false
|
||||||
|
self.skewer = t.skewer 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
|
||||||
|
|
@ -134,6 +136,14 @@ function Entity:canMove()
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Entity:isSticky()
|
||||||
|
return self.sticky
|
||||||
|
end
|
||||||
|
|
||||||
|
function Entity:isSkewer()
|
||||||
|
return self.skewer
|
||||||
|
end
|
||||||
|
|
||||||
function Entity:canPassOver()
|
function Entity:canPassOver()
|
||||||
if self:getCollision() == "none" then
|
if self:getCollision() == "none" then
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
89
generic_skewer.lua
Normal file
89
generic_skewer.lua
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
require "entity"
|
||||||
|
|
||||||
|
GenericSkewer = class("GenericSkewer", Entity)
|
||||||
|
|
||||||
|
local edgeDirections = {
|
||||||
|
{x = 1, y = 0}, {x = -1, y = 0},
|
||||||
|
{x = 0, y = 1}, {x = 0, y = -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function shapeHasCell(shape, x, y)
|
||||||
|
for _, cell in ipairs(shape) do
|
||||||
|
if cell.x == x and cell.y == y then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
function drawSkewerSprite(sprite, cellShape, x, y, scale)
|
||||||
|
cellShape = cellShape or {{x = 1, y = 1}}
|
||||||
|
local cellSize = 16 * scale
|
||||||
|
local inset = scale
|
||||||
|
local reach = 3 * scale
|
||||||
|
local halfBase = 1.5 * scale
|
||||||
|
|
||||||
|
for _, cell in ipairs(cellShape) do
|
||||||
|
local left = x + (cell.x - 1) * cellSize
|
||||||
|
local top = y + (cell.y - 1) * cellSize
|
||||||
|
for _, direction in ipairs(edgeDirections) do
|
||||||
|
if not shapeHasCell(cellShape, cell.x + direction.x, cell.y + direction.y) then
|
||||||
|
for _, along in ipairs({5, 11}) do
|
||||||
|
if direction.x ~= 0 then
|
||||||
|
local edgeX = direction.x > 0 and left + cellSize - inset or left + inset
|
||||||
|
local centerY = top + along * scale
|
||||||
|
love.graphics.polygon("fill",
|
||||||
|
edgeX, centerY - halfBase,
|
||||||
|
edgeX + direction.x * reach, centerY,
|
||||||
|
edgeX, centerY + halfBase
|
||||||
|
)
|
||||||
|
else
|
||||||
|
local edgeY = direction.y > 0 and top + cellSize - inset or top + inset
|
||||||
|
local centerX = left + along * scale
|
||||||
|
love.graphics.polygon("fill",
|
||||||
|
centerX - halfBase, edgeY,
|
||||||
|
centerX, edgeY + direction.y * reach,
|
||||||
|
centerX + halfBase, edgeY
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
love.graphics.draw(sprite, x, y, 0, scale, scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSkewer:initialize(x, y, color, sprite, name, cellShape)
|
||||||
|
Entity.initialize(self, {
|
||||||
|
name = name,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
color = color,
|
||||||
|
collision = "moveable",
|
||||||
|
skewer = true,
|
||||||
|
sprite = sprite,
|
||||||
|
cellShape = cellShape
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSkewer:update()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSkewer:draw(channel)
|
||||||
|
local sprite = self.sprites[channel]
|
||||||
|
if not sprite then return end
|
||||||
|
love.graphics.setColor(gColor[channel]:set())
|
||||||
|
local p = self:getDrawPos()
|
||||||
|
drawSkewerSprite(
|
||||||
|
sprite, self:getCellShape(),
|
||||||
|
p.x + self.drawOffset.x * drawScale,
|
||||||
|
p.y + self.drawOffset.y * drawScale,
|
||||||
|
drawScale
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSkewer:tick()
|
||||||
|
|
||||||
|
end
|
||||||
42
generic_sticky.lua
Normal file
42
generic_sticky.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
require "entity"
|
||||||
|
|
||||||
|
GenericSticky = class("GenericSticky", Entity)
|
||||||
|
|
||||||
|
function GenericSticky:initialize(x, y, color, sprite, name, cellShape)
|
||||||
|
Entity.initialize(self, {
|
||||||
|
name = name,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
color = color,
|
||||||
|
collision = "moveable",
|
||||||
|
sticky = true,
|
||||||
|
sprite = sprite,
|
||||||
|
cellShape = cellShape
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSticky:update()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSticky:draw(channel)
|
||||||
|
local sprite = self.sprites[channel]
|
||||||
|
if not sprite then return end
|
||||||
|
if not drawGoopySprite then
|
||||||
|
Entity.draw(self, channel)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
love.graphics.setColor(gColor[channel]:set())
|
||||||
|
local p = self:getDrawPos()
|
||||||
|
drawGoopySprite(
|
||||||
|
sprite,
|
||||||
|
p.x + self.drawOffset.x * drawScale,
|
||||||
|
p.y + self.drawOffset.y * drawScale,
|
||||||
|
drawScale
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GenericSticky:tick()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
@ -233,6 +233,9 @@ local function rebuildAssetSearchButtons()
|
||||||
for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end
|
for color, location in pairs(gameAssets[asset].sprites) do sprites[color] = love.graphics.newImage(location) end
|
||||||
table.insert(assetSearchButtons, createUIElement{
|
table.insert(assetSearchButtons, createUIElement{
|
||||||
sprites = sprites,
|
sprites = sprites,
|
||||||
|
goopy = gameAssets[asset].goopy,
|
||||||
|
skewer = gameAssets[asset].skewer,
|
||||||
|
cells = gameAssets[asset].cells,
|
||||||
name = "asset",
|
name = "asset",
|
||||||
text = asset,
|
text = asset,
|
||||||
x = 30,
|
x = 30,
|
||||||
|
|
@ -529,13 +532,18 @@ function selectFolder(folder)
|
||||||
-- each frame from the list section (see buildRoomLayout).
|
-- each frame from the list section (see buildRoomLayout).
|
||||||
table.insert(assetListButtons, createUIElement{
|
table.insert(assetListButtons, createUIElement{
|
||||||
sprites = sprites,
|
sprites = sprites,
|
||||||
|
goopy = gameAssets[asset].goopy,
|
||||||
|
skewer = gameAssets[asset].skewer,
|
||||||
|
cells = gameAssets[asset].cells,
|
||||||
name = "asset",
|
name = "asset",
|
||||||
text = asset,
|
text = asset,
|
||||||
x = 30,
|
x = 30,
|
||||||
y = 0,
|
y = 0,
|
||||||
onClic = {
|
onClic = {
|
||||||
left = function() selectAsset(asset) end,
|
left = function() selectAsset(asset) end,
|
||||||
right = function() editAsset(asset) end,
|
right = function()
|
||||||
|
if not gameAssets[asset].synthetic then editAsset(asset) end
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -592,6 +600,9 @@ function editAsset(asset)
|
||||||
local assetButton = createUIElement{
|
local assetButton = createUIElement{
|
||||||
name = asset,
|
name = asset,
|
||||||
sprites = assetSprites,
|
sprites = assetSprites,
|
||||||
|
goopy = assetProperties.goopy,
|
||||||
|
skewer = assetProperties.skewer,
|
||||||
|
cells = assetProperties.cells,
|
||||||
x = sb.w / 2 - 18,
|
x = sb.w / 2 - 18,
|
||||||
y = 80,
|
y = 80,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ function GUI:initialize(t)
|
||||||
self.offset = {x = 0, y = 0}
|
self.offset = {x = 0, y = 0}
|
||||||
self.name = t.name
|
self.name = t.name
|
||||||
self.text = t.text
|
self.text = t.text
|
||||||
|
self.goopy = t.goopy or false
|
||||||
|
self.skewer = t.skewer or false
|
||||||
|
self.cells = t.cells
|
||||||
|
|
||||||
if not t.onClic then
|
if not t.onClic then
|
||||||
t.onClic = {}
|
t.onClic = {}
|
||||||
|
|
@ -99,7 +102,13 @@ function GUI:draw()
|
||||||
if self.sprites then
|
if self.sprites then
|
||||||
for color, sprite in pairs(self.sprites) do
|
for color, sprite in pairs(self.sprites) do
|
||||||
love.graphics.setColor(gColor[color]:set())
|
love.graphics.setColor(gColor[color]:set())
|
||||||
love.graphics.draw(sprite, box.x, box.y, 0, self.scale, self.scale)
|
if self.goopy and drawGoopySprite then
|
||||||
|
drawGoopySprite(sprite, box.x, box.y, self.scale)
|
||||||
|
elseif self.skewer and drawSkewerSprite then
|
||||||
|
drawSkewerSprite(sprite, self.cells and self.cells[color], box.x, box.y, self.scale)
|
||||||
|
else
|
||||||
|
love.graphics.draw(sprite, box.x, box.y, 0, self.scale, self.scale)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
require "libs/TSerial"
|
require "libs/TSerial"
|
||||||
|
|
||||||
|
local BoxVariants = require "box_variants"
|
||||||
|
|
||||||
local targetFolder = "art"
|
local targetFolder = "art"
|
||||||
local reanalyzeCells = true
|
local reanalyzeCells = true
|
||||||
|
|
||||||
|
|
@ -57,5 +59,5 @@ function getAllAssets()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return globalAssetProperties
|
return BoxVariants.addTo(globalAssetProperties)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
83
movement.lua
Normal file
83
movement.lua
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
-- 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
|
||||||
|
-- pushing), is behind a directional sticky puller, or touches a skewer block.
|
||||||
|
local Movement = {}
|
||||||
|
|
||||||
|
local adjacentDirections = {
|
||||||
|
{x = 1, y = 0},
|
||||||
|
{x = -1, y = 0},
|
||||||
|
{x = 0, y = 1},
|
||||||
|
{x = 0, y = -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isSticky(entity)
|
||||||
|
return entity.isSticky and entity:isSticky()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isPassable(entity)
|
||||||
|
return entity.canPassOver and entity:canPassOver()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isSkewer(entity)
|
||||||
|
return entity.isSkewer and entity:isSkewer()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
|
||||||
|
local entities = {}
|
||||||
|
local pendingCells = {}
|
||||||
|
for _, cell in ipairs(startCells) do
|
||||||
|
table.insert(pendingCells, cell)
|
||||||
|
end
|
||||||
|
|
||||||
|
local nextCell = 1
|
||||||
|
while nextCell <= #pendingCells do
|
||||||
|
local cell = pendingCells[nextCell]
|
||||||
|
nextCell = nextCell + 1
|
||||||
|
|
||||||
|
if not isInBounds(cell) then
|
||||||
|
return false, entities
|
||||||
|
end
|
||||||
|
|
||||||
|
local entity = entityAt(cell)
|
||||||
|
if entity and not isPassable(entity) and not entities[entity] then
|
||||||
|
if not entity:canMove() then
|
||||||
|
return false, entities
|
||||||
|
end
|
||||||
|
|
||||||
|
entities[entity] = entity
|
||||||
|
|
||||||
|
-- Anything occupying this block's destination must be pushed too.
|
||||||
|
for _, projectedCell in ipairs(entity:getOccupiedCells(direction)) do
|
||||||
|
table.insert(pendingCells, projectedCell)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sticky blocks pull only from the trailing face, like the player. A
|
||||||
|
-- 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 _, 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 attached = adjacentEntity and (
|
||||||
|
isSkewer(entity) or isSkewer(adjacentEntity) or
|
||||||
|
(trailing and (isSticky(entity) or isSticky(adjacentEntity)))
|
||||||
|
)
|
||||||
|
if attached and adjacentEntity ~= entity and adjacentEntity:canMove() then
|
||||||
|
table.insert(pendingCells, adjacentCell)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, entities
|
||||||
|
end
|
||||||
|
|
||||||
|
return Movement
|
||||||
124
room.lua
124
room.lua
|
|
@ -10,8 +10,12 @@ require "switch"
|
||||||
require "door"
|
require "door"
|
||||||
require "dialogue_manager"
|
require "dialogue_manager"
|
||||||
require "generic_moveable"
|
require "generic_moveable"
|
||||||
|
require "generic_sticky"
|
||||||
|
require "generic_skewer"
|
||||||
require "generic_unmoveable"
|
require "generic_unmoveable"
|
||||||
|
|
||||||
|
local Movement = require "movement"
|
||||||
|
|
||||||
function Room:initialize(t)
|
function Room:initialize(t)
|
||||||
self.player = {}
|
self.player = {}
|
||||||
self.switches = { red = {}, green = {}, blue = {} }
|
self.switches = { red = {}, green = {}, blue = {} }
|
||||||
|
|
@ -202,7 +206,7 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
local move = true
|
local move = true
|
||||||
-- PUSHHHHHHHHHHHHHHHHHHHHHH---------------------
|
-- PUSH ---------------------------------------------------------------
|
||||||
|
|
||||||
local initialTargetCell = {
|
local initialTargetCell = {
|
||||||
x = pos.x + dir.x, y = pos.y + dir.y
|
x = pos.x + dir.x, y = pos.y + dir.y
|
||||||
|
|
@ -219,104 +223,24 @@ function Room:movePlayer(key)
|
||||||
if not self:isInBounds(initialTargetCell) then
|
if not self:isInBounds(initialTargetCell) then
|
||||||
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
||||||
move = true
|
move = true
|
||||||
elseif initialTargetEntity and initialTargetEntity:canPassOver() then
|
|
||||||
--we should put this in the else statement actually
|
|
||||||
move = true
|
|
||||||
else
|
else
|
||||||
local targetCells
|
move, entitiesToPush = Movement.findMovableGroup(
|
||||||
|
{initialTargetCell}, dir,
|
||||||
targetCells = {initialTargetCell} -- ...we start with our attempted movement...
|
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
||||||
local doneSearching
|
function(cell) return self:isInBounds(cell) end
|
||||||
while not doneSearching do
|
)
|
||||||
nextTargetCells = {} -- ...then we create this array to save our next group of cells
|
|
||||||
local emptyCells = {}
|
|
||||||
for _, cell in pairs(targetCells) do
|
|
||||||
if self:isInBounds(cell) then
|
|
||||||
local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
|
|
||||||
if entity then
|
|
||||||
--something is here!!
|
|
||||||
if not entitiesToPush[entity] then --if the entity hasnt been found already
|
|
||||||
if entity:canMove() then
|
|
||||||
local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
|
|
||||||
entitiesToPush[entity] = entity
|
|
||||||
for _, projectedCell in ipairs(projectedCells) do
|
|
||||||
table.insert(nextTargetCells, projectedCell)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
move = false
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
--there's nothin there!!
|
|
||||||
table.insert(emptyCells, cell)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
--we are out of bounds now
|
|
||||||
move = false
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--if every projected cell is empty then we can do the move
|
|
||||||
if #emptyCells == #targetCells then
|
|
||||||
move = true
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
|
|
||||||
targetCells = nextTargetCells
|
|
||||||
end
|
|
||||||
--let's get out of this loop b/c if we can't move one color we can't move at all
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- PULL ---------------------------------------------------------------
|
||||||
---PULL CHEC
|
|
||||||
|
|
||||||
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
||||||
|
local pull = false
|
||||||
local targetCells
|
if self:isInBounds(initialTargetCell) then
|
||||||
|
pull, entitiesToPull = Movement.findMovableGroup(
|
||||||
local pull
|
{initialTargetCell}, dir,
|
||||||
--for each color...
|
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
||||||
targetCells = {initialTargetCell} -- ...we start with our attempted movement...
|
function(cell) return self:isInBounds(cell) end
|
||||||
local doneSearching
|
)
|
||||||
while not doneSearching do
|
|
||||||
nextTargetCells = {} -- ...then we create this array to save our next group of cells
|
|
||||||
local emptyCells = {}
|
|
||||||
for _, cell in pairs(targetCells) do
|
|
||||||
if self:isInBounds(cell) then
|
|
||||||
local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
|
|
||||||
if entity then
|
|
||||||
--something is here!!
|
|
||||||
if not entitiesToPull[entity] then --if the entity hasnt been found already
|
|
||||||
if entity:canMove() then
|
|
||||||
local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
|
|
||||||
entitiesToPull[entity] = entity
|
|
||||||
for _, projectedCell in ipairs(projectedCells) do
|
|
||||||
table.insert(nextTargetCells, projectedCell)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
pull = false
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
--there's nothin there!!
|
|
||||||
table.insert(emptyCells, cell)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
--we are out of bounds now
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--if every projected cell is empty then we can do the move
|
|
||||||
if #emptyCells == #targetCells then
|
|
||||||
pull = true
|
|
||||||
doneSearching = true
|
|
||||||
end
|
|
||||||
|
|
||||||
targetCells = nextTargetCells
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if move then
|
if move then
|
||||||
|
|
@ -325,7 +249,11 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
if pull == true then
|
if pull == true then
|
||||||
for _, entity in pairs(entitiesToPull) do
|
for _, entity in pairs(entitiesToPull) do
|
||||||
entity:move(axis, dir[axis])
|
-- A connected component can wrap around the player and be found by
|
||||||
|
-- both searches. Every entity still moves exactly one grid cell.
|
||||||
|
if not entitiesToPush[entity] then
|
||||||
|
entity:move(axis, dir[axis])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self.player[color]:move(axis, dir[axis])
|
self.player[color]:move(axis, dir[axis])
|
||||||
|
|
@ -537,7 +465,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"); anything without a .meta defaults to immoveable. npc is the one
|
-- "moveable"/"sticky"/"skewer"); 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
|
||||||
|
|
@ -577,6 +505,10 @@ function Room:createEntity(class, color, x, y)
|
||||||
if cellShape and #cellShape == 0 then cellShape = nil end
|
if cellShape and #cellShape == 0 then cellShape = nil end
|
||||||
if behavior == "moveable" then
|
if behavior == "moveable" then
|
||||||
entity = GenericMoveable:new(x, y, color, sprite, class, cellShape)
|
entity = GenericMoveable:new(x, y, color, sprite, class, cellShape)
|
||||||
|
elseif behavior == "sticky" then
|
||||||
|
entity = GenericSticky:new(x, y, color, sprite, class, cellShape)
|
||||||
|
elseif behavior == "skewer" then
|
||||||
|
entity = GenericSkewer: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
|
||||||
|
|
|
||||||
47
shaders.lua
47
shaders.lua
|
|
@ -59,6 +59,53 @@ glow = love.graphics.newShader[[
|
||||||
}
|
}
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
-- Sticky boxes reuse the authored box textures. This shader only wobbles their
|
||||||
|
-- sampling; the draw helper adds a tiny squash-and-stretch bounce around center.
|
||||||
|
goopShader = love.graphics.newShader[[
|
||||||
|
extern number goopTime;
|
||||||
|
extern vec2 sourceSize;
|
||||||
|
|
||||||
|
float goopHash(float value) {
|
||||||
|
return fract(sin(value * 91.3458) * 47453.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) {
|
||||||
|
float sourceY = textureCoords.y;
|
||||||
|
float pixelWidth = 1.0 / sourceSize.x;
|
||||||
|
float pixelHeight = 1.0 / sourceSize.y;
|
||||||
|
float column = floor(textureCoords.x * sourceSize.x / 2.0);
|
||||||
|
float seed = goopHash(column);
|
||||||
|
|
||||||
|
// Two gentle, out-of-phase waves make the surface feel soft and viscous.
|
||||||
|
// Both stay sub-pixel so the authored silhouette remains recognizable.
|
||||||
|
float horizontalWave = sin(goopTime * 1.7 + sourceY * 10.0 + seed * 6.2831);
|
||||||
|
float verticalWave = sin(goopTime * 1.3 + textureCoords.x * 12.0 + seed * 3.1415);
|
||||||
|
float sourceX = clamp(textureCoords.x + horizontalWave * pixelWidth * 0.45,
|
||||||
|
pixelWidth * 0.5, 1.0 - pixelWidth * 0.5);
|
||||||
|
float wobbledY = clamp(sourceY + verticalWave * pixelHeight * 0.28,
|
||||||
|
pixelHeight * 0.5, 1.0 - pixelHeight * 0.5);
|
||||||
|
|
||||||
|
return Texel(texture, vec2(sourceX, wobbledY)) * color;
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
|
||||||
|
function drawGoopySprite(sprite, x, y, scale)
|
||||||
|
local w, h = sprite:getWidth(), sprite:getHeight()
|
||||||
|
local time = love.timer.getTime()
|
||||||
|
local bounce = math.sin(time * 2.4 + w * .17 + h * .11) * .035
|
||||||
|
local scaleX = scale * (1 + bounce)
|
||||||
|
local scaleY = scale * (1 - bounce)
|
||||||
|
local drawX = x - w * (scaleX - scale) / 2
|
||||||
|
local drawY = y - h * (scaleY - scale) / 2
|
||||||
|
|
||||||
|
goopShader:send("goopTime", time)
|
||||||
|
goopShader:send("sourceSize", {w, h})
|
||||||
|
local previousShader = love.graphics.getShader()
|
||||||
|
love.graphics.setShader(goopShader)
|
||||||
|
love.graphics.draw(sprite, drawX, drawY, 0, scaleX, scaleY)
|
||||||
|
love.graphics.setShader(previousShader)
|
||||||
|
end
|
||||||
|
|
||||||
-- Accessibility post-process. The puzzle's RGB channels are normally also
|
-- Accessibility post-process. The puzzle's RGB channels are normally also
|
||||||
-- its visual language, so this runs only after the entire scene has been
|
-- its visual language, so this runs only after the entire scene has been
|
||||||
-- composed. It leaves neutral UI/text alone and remaps saturated colours to
|
-- composed. It leaves neutral UI/text alone and remaps saturated colours to
|
||||||
|
|
|
||||||
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)
|
||||||
|
|
@ -171,7 +171,14 @@ function World:drawRoomPreview(name, x, y, size)
|
||||||
local path = props.sprites[color]
|
local path = props.sprites[color]
|
||||||
if path then
|
if path then
|
||||||
love.graphics.setColor(gColor[color]:set())
|
love.graphics.setColor(gColor[color]:set())
|
||||||
love.graphics.draw(loadSprite(path), x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
local sprite = loadSprite(path)
|
||||||
|
if props.goopy and drawGoopySprite then
|
||||||
|
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
||||||
|
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)
|
||||||
|
else
|
||||||
|
love.graphics.draw(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue