Collapse the spiky/sticky/skewer box-variant assets into a single per-
instance `adhesion` property on any moveable block, chosen in the editor
via a dropdown next to the colour picker and stored in the .sav alongside
the class.
- spiky -> sticky: soft grab on every true edge (goopy shader)
- skewer -> gluey: rigid weld on every true edge (spiked outline)
- old trailing-face sticky is removed; its saves fold into new sticky
Move the spike geometry + drawGlueySprite into shaders.lua, fold drawing
and the adhesion flag into GenericMoveable, and delete the three
Generic{Sticky,Spiky,Skewer} classes and box_variants.lua. Room:createEntity
normalises legacy spiky_/skewer_/sticky_ class prefixes so old saves load,
and serialization emits `adhesion`. World/map previews and the movement
proxy follow the rename.
Backfill rooms/*.sav to the new format via tools/backfill_adhesion.lua
(41 objects across 8 files). Update tests to the two-mode model; the two
tests asserting the removed lateral-shear limitation are dropped and a
legacy-load migration test added. 27/27 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
4.2 KiB
Lua
137 lines
4.2 KiB
Lua
-- 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 softly grabbed by a sticky block, or is rigidly welded to a
|
|
-- gluey 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 isGluey(entity)
|
|
return entity.isGluey and entity:isGluey()
|
|
end
|
|
|
|
-- Build one rigid component: ordinary pushes are required, and gluey blocks weld
|
|
-- 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 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] and
|
|
not (alreadyMoving and alreadyMoving[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
|
|
|
|
-- Gluey blocks make every edge contact part of this rigid component.
|
|
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 attached = adjacentEntity and (
|
|
isGluey(entity) or isGluey(adjacentEntity)
|
|
)
|
|
if attached and adjacentEntity ~= entity and adjacentEntity:canMove() and
|
|
not (alreadyMoving and alreadyMoving[adjacentEntity]) then
|
|
table.insert(pendingCells, adjacentCell)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return true, entities
|
|
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 on every face: it tries to drag each neighbour
|
|
-- along, but 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 softContact = adjacentEntity 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
|