Replace box variants with a moveable adhesion property

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>
This commit is contained in:
Your Name 2026-08-21 16:34:01 -04:00
parent c348b81a93
commit 7d84f595e4
25 changed files with 4010 additions and 3797 deletions

View file

@ -1,37 +0,0 @@
local BoxVariants = {}
-- Add directional sticky-puller, rigid skewer, and soft all-edge spiky variants
-- for every authored moveable box. All share sprites/collision 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")
addVariant(base, "spiky_", "spiky", "spiky", "spiky")
end
return assets
end
return BoxVariants

View file

@ -0,0 +1,247 @@
# Adhesion refactor — plan
## Goal
Today "spiky", "sticky", "skewer" are **separate box assets** (distinct `class`
names, generated by `box_variants.lua`). Replace that with a single per-instance
**adhesion property** on any *moveable* entity, chosen at placement time via a
dropdown in the level editor.
### Renames / semantics (final)
- **old `spiky` → new `sticky`** — soft grab on **every true edge**. When the block
moves, it *tries* to drag every orthogonally-adjacent block along; if a dragged
block is blocked, that branch just shears off (the move still succeeds). Renders
with the goopy shader.
- **old `skewer` → new `gluey`** — rigid weld on **every true edge**. Welded
neighbors move as one component; if any can't move, the whole move fails.
Renders with the spike/teeth outline.
- **old `sticky` (trailing-face-only grab) → DELETED.** Its distinct mechanic is
gone. Legacy `sticky_*` assets in saves are absorbed into new `sticky`.
- **regular** — plain moveable, no adhesion.
Adhesion is stored as `adhesion = "sticky" | "gluey"` (nil/absent = regular) and
is **only meaningful on moveable entities**.
### Contour requirement (already satisfied — no work)
Edges must follow the *true* profile of multi-cell shapes with holes.
- Spikes: `drawSpikes` (currently in `generic_skewer.lua`) already walks
`cellShape` and only draws teeth on an edge with no neighbor cell. Keep as-is.
- Goopy: `drawGoopySprite`/`goopShader` (`shaders.lua`) sample the sprite's own
alpha, so transparent cells stay transparent. Nothing to change.
## Rationale / approach
The behavior already keys off boolean flags read through `Entity` methods
(`isSticky`/`isSkewer`/`isSpiky`), and the three `Generic*` subclasses only differ
by which flag they set + which draw helper they call. So collapsing them into one
`GenericMoveable` carrying an `adhesion` string is a small, mechanical change. The
real surface area is: (a) the movement rules, (b) serialization + backfilling
existing `.sav`s, (c) deleting the synthetic box-variant assets, (d) the editor
dropdown. Do it in that order so tests stay runnable (`lua tests/test_movement.lua`)
throughout.
---
## 1. `entity.lua`
- `initialize` (lines 17–19): delete the `self.sticky/skewer/spiky = ...` trio,
replace with:
```lua
-- Per-instance adhesion for moveable blocks: "sticky" (soft grab on every true
-- edge) or "gluey" (rigid weld). nil = plain moveable. See docs/adhesion-*.
self.adhesion = t.adhesion
```
- Methods (lines 140–150): replace the three predicates with:
```lua
function Entity:isSticky() return self.adhesion == "sticky" end
function Entity:isGluey() return self.adhesion == "gluey" end
```
Remove `isSkewer` and `isSpiky`.
## 2. `movement.lua`
Semantics: `sticky` = soft all-edge pull; `gluey` = rigid all-edge weld. The old
"trailing face only" behavior is deleted.
- Local helpers (13–27): keep `isSticky`; rename `isSkewer`→`isGluey`
(`entity.isGluey and entity:isGluey()`); delete `isSpiky`.
- `findRigidGroup` (line 72): `isSkewer(entity) or isSkewer(adjacentEntity)` →
`isGluey(entity) or isGluey(adjacentEntity)`. Update the comment (61–62) to say
"gluey welds every edge".
- Pull loop (102–118): remove the `trailing` computation and the old sticky
branch. `softContact` becomes just:
```lua
local softContact = adjacentEntity and
(isSticky(entity) or isSticky(adjacentEntity))
```
Update comments (2–3, 102–104) to describe sticky = soft on every edge, gluey =
rigid.
## 3. Draw helpers → `shaders.lua`
`drawGoopySprite` already lives here. Move the spike geometry here too so the
`Generic*` box files can be deleted:
- Move `edgeDirections`, `shapeHasCell`, `drawSpikes` from `generic_skewer.lua`
into `shaders.lua` (verbatim; they're pure geometry).
- Add `drawGlueySprite(sprite, cellShape, x, y, scale)` = old `drawSkewerSprite`
(calls `drawSpikes` then draws the sprite).
- Drop `drawSpikySprite` entirely (old spiky's draw just fell back to goopy; new
sticky uses `drawGoopySprite` directly).
## 4. `generic_moveable.lua` (the one surviving box class)
- `initialize(x, y, color, sprite, name, cellShape, adhesion)` — pass
`adhesion = adhesion` into `Entity.initialize`.
- Add a `draw(channel)` that branches:
```lua
function GenericMoveable:draw(channel)
local sprite = self.sprites[channel]; if not sprite then return end
love.graphics.setColor(gColor[channel]:set())
local p = self:getDrawPos()
local x = p.x + self.drawOffset.x * drawScale
local y = p.y + self.drawOffset.y * drawScale
if self.adhesion == "gluey" then
drawGlueySprite(sprite, self:getCellShape(), x, y, drawScale)
elseif self.adhesion == "sticky" and drawGoopySprite then
drawGoopySprite(sprite, x, y, drawScale)
else
love.graphics.draw(sprite, x, y, 0, drawScale, drawScale)
end
end
```
## 5. Delete files + requires
- Delete `generic_sticky.lua`, `generic_spiky.lua`, `generic_skewer.lua`.
- `room.lua` (15–17): remove the three `require "generic_*"` lines (keep
`generic_moveable`).
- Delete `box_variants.lua`.
- `level_editor/object_attributes.lua`: remove line 3 `require "box_variants"`;
change line 80 `return BoxVariants.addTo(globalAssetProperties)` →
`return globalAssetProperties`.
## 6. `room.lua` `createEntity` (752–849)
Boxes are now always `behavior == "moveable"`; adhesion comes from `data.adhesion`.
- After the note/mark/npc early-returns and **before** `local props = ...`,
normalize legacy variant class names so old saves load:
```lua
-- Legacy: adhesion used to be baked into separate asset classes
-- (spiky_/skewer_/sticky_<box>). Collapse to the base asset + an adhesion string.
local adhesion = data.adhesion
local legacy = { spiky = "sticky", skewer = "gluey", sticky = "sticky" }
for prefix, mapped in pairs(legacy) do
local base = class:match("^" .. prefix .. "_(.+)")
if base then class = base; adhesion = adhesion or mapped; break end
end
```
- In the sprite branch (831–841): keep only
```lua
elseif behavior == "moveable" then
entity = GenericMoveable:new(x, y, color, sprite, class, cellShape, adhesion)
```
Delete the `"sticky"`/`"skewer"`/`"spiky"` elseif branches. (adhesion is only
passed here, so it can never attach to a non-moveable.)
- `entity.saveClass = class` at 847 now stores the base box name — good.
## 7. Serialization
- `serialize` `emit` (1180–1185): add
```lua
if entity.adhesion then object.adhesion = entity.adhesion end
```
- `deserialize` (1114) already forwards the whole `obj` as `data`, so
`data.adhesion` is read automatically. No change.
## 8. Backfill existing `.sav`s (one-off script)
`rooms/*.sav` reference old class names (`spiky_box`, `skewer_box`, `sticky_box`,
plus shape suffixes like `spiky_box_2x1_11`). `TSerial` is pure Lua and runs under
plain `lua`. Write `tools/backfill_adhesion.lua`:
- For every file in `rooms/`, `TSerial.unpack` it, walk `saveTable.objects`, and for
each object whose `class` matches `^spiky_(.+)`/`^skewer_(.+)/^sticky_(.+)`:
set `object.class = base`, `object.adhesion = {spiky="sticky", skewer="gluey",
sticky="sticky"}[prefix]`.
- Re-emit with `TSerial.pack(saveTable, nil, true)` and overwrite the file.
- Affected files (from grep): `village_1/2/3.sav`, `world.sav`,
`spiky_test.sav`, `spiky_test_2.sav`, `spiky_test_3.sav`, `testing_sticky.sav`,
`testing_sticky_3.sav`, `testing_sticky_4.sav` (process all, harmless on others).
- Run `lua tools/backfill_adhesion.lua`; commit the rewritten saves. (Note: the
`.sav` filenames like `spiky_test` are just level names — leave them, or rename
separately; only the object `class` fields matter.)
## 9. Editor — adhesion dropdown next to the color picker
`level_editor/editor.lua`. Add module-level state near the other locals (~62):
```lua
local selectedAdhesion = "regular" -- "regular" | "sticky" | "gluey"
local adhesionMenuOpen = false
local adhesionRect = nil -- set each frame by layoutRoomControls
```
- **Palette sections** `rebuildBoxPaletteSections` (134–148): drop the
`spiky/sticky/skewer` prefixes; keep only `{ "box" }` (+ the `barrier` section).
The single box section now lists just the base box shapes.
- **Layout** `layoutRoomControls` (284–338): the color row is
`placeRow({ "white","red","green","blue" }, 22)`. Put the dropdown to the right
of it, on the same 22px row (vertical budget is tight: `controlsH = 130`). Easiest:
before `placeRow` for colors, compute the color row's y, lay the 4 swatches, then
set `adhesionRect = { x = <right of swatches + gap>, y = rowY, w = 84, h = 22 }`.
If width is tight, bump `controlsH` to ~150 in `roomSections` (261) and add a
dedicated row instead — either is fine; "next to the color picker" is the intent.
- **Draw** (in `editorDraw`, room branch, after the control cluster ~1834): draw
`adhesionRect` as a labeled box showing `selectedAdhesion:upper()`. When
`adhesionMenuOpen`, draw the 3 options stacked **upward** from `adhesionRect`
(it's near the bottom edge): `regular`, `sticky`, `gluey`. Highlight the current.
- **Click** (in `editorMouseHandler`, room branch — add before the control-cluster
loop ~1520): if `adhesionMenuOpen` and click hits an option row → set
`selectedAdhesion`, close, return. If click hits `adhesionRect` → toggle
`adhesionMenuOpen`, return. Any other click closes the menu.
- **Placement threading**: `paintRoomCell` (88–94) calls
`attemptPlaceAtCell(selectedAsset, gameAssets[selectedAsset], cell, selectedColor)`.
Pass adhesion: add a 6th arg `selectedAdhesion ~= "regular" and selectedAdhesion or nil`.
- `room.lua Room:attemptPlaceAtCell(name, props, cell, color, text, adhesion)`
(1041): forward it into the per-channel `createEntity`:
`self:createEntity(name, c, cell.x, cell.y, { adhesion = adhesion })` (1087).
(adhesion is ignored by non-moveable classes since only the moveable branch
reads it.)
- **Cleanup of dead flag plumbing** (optional but tidy): `props.goopy/skewer/spiky`
no longer exist. Remove those branches in `drawPaletteAsset` (216–224) — draw the
sprite plainly (the palette shows base boxes; adhesion is a placement mode, not a
per-asset style). Remove `goopy/skewer/spiky/cells` args passed to
`createUIElement` at the asset-button/quick-button/search sites (403–414,
744–749, 820–836, 1147–1156) and the matching fields + draw branches in
`level_editor/gui.lua` (12–14, 103–116). Harmless if left (nil → falsy), so this
can be deferred.
## 10. Tests (`tests/test_movement.lua`)
- Helper `movable` (32–57): change signature to `movable(x, y, adhesion, shape)`;
store `self.adhesion`; replace `isSticky/isSkewer/isSpiky` with
`isSticky() -> adhesion=="sticky"`, `isGluey() -> adhesion=="gluey"`.
Update the existing callers that passed positional `sticky/skewer/spiky`.
- Rename the "spiky" test (536–552): call `movable(4, 4, "sticky")` for the grabby
block; assertions unchanged (soft shear-off behavior is identical).
- If there are `skewer`-based tests elsewhere, pass `"gluey"`.
- Delete the "every moveable base box gets ... variants" test (554–604) — that
feature (`box_variants.lua`) is gone. Update the final `print` (606) to drop
"block-variant".
- `lua tests/test_movement.lua` must pass.
## Sanity checklist
- [ ] `grep -rn "spiky\|skewer\|isSpiky\|isSkewer\|box_variants\|BoxVariants"` returns
nothing in `.lua` (except maybe level-name strings / this doc).
- [ ] `grep -rn "adhesion"` shows entity/movement/room/editor wired consistently.
- [ ] Old saves load (backfilled) and re-save with `adhesion` on the right objects.
- [ ] Placing with the dropdown on `sticky`/`gluey` produces the goopy/spiked look
and the correct push/pull behavior; on a non-moveable asset it's ignored.

View file

@ -14,9 +14,10 @@ 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 -- Per-instance adhesion for moveable blocks: "sticky" (soft grab on every true
self.skewer = t.skewer or false -- edge) or "gluey" (rigid weld on every true edge). nil = plain moveable.
self.spiky = t.spiky or false -- See docs/adhesion-refactor-plan.md.
self.adhesion = t.adhesion
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
@ -138,15 +139,11 @@ function Entity:canMove()
end end
function Entity:isSticky() function Entity:isSticky()
return self.sticky return self.adhesion == "sticky"
end end
function Entity:isSkewer() function Entity:isGluey()
return self.skewer return self.adhesion == "gluey"
end
function Entity:isSpiky()
return self.spiky
end end
function Entity:canPassOver() function Entity:canPassOver()

View file

@ -2,7 +2,7 @@ require "entity"
GenericMoveable = class("GenericMoveable", Entity) GenericMoveable = class("GenericMoveable", Entity)
function GenericMoveable:initialize(x, y, color, sprite, name, cellShape) function GenericMoveable:initialize(x, y, color, sprite, name, cellShape, adhesion)
Entity.initialize(self, { Entity.initialize(self, {
name = name, name = name,
x = x, x = x,
@ -10,7 +10,8 @@ function GenericMoveable:initialize(x, y, color, sprite, name, cellShape)
color = color, color = color,
collision = "moveable", collision = "moveable",
sprite = sprite, sprite = sprite,
cellShape = cellShape cellShape = cellShape,
adhesion = adhesion
}) })
end end
@ -18,6 +19,25 @@ function GenericMoveable:update()
end end
-- Adhesion drives the look as well as the mechanic: gluey blocks get spiked
-- teeth on their true contour, sticky blocks get the wobbling goopy shader, and
-- plain moveables draw normally.
function GenericMoveable:draw(channel)
local sprite = self.sprites[channel]
if not sprite then return end
love.graphics.setColor(gColor[channel]:set())
local p = self:getDrawPos()
local x = p.x + self.drawOffset.x * drawScale
local y = p.y + self.drawOffset.y * drawScale
if self.adhesion == "gluey" and drawGlueySprite then
drawGlueySprite(sprite, self:getCellShape(), x, y, drawScale)
elseif self.adhesion == "sticky" and drawGoopySprite then
drawGoopySprite(sprite, x, y, drawScale)
else
love.graphics.draw(sprite, x, y, 0, drawScale, drawScale)
end
end
function GenericMoveable:tick() function GenericMoveable:tick()
end end

View file

@ -1,92 +0,0 @@
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 drawSpikes(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
end
function drawSkewerSprite(sprite, cellShape, x, y, scale)
drawSpikes(cellShape, x, y, scale)
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

View file

@ -1,46 +0,0 @@
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

View file

@ -1,42 +0,0 @@
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

View file

@ -63,6 +63,15 @@ local boxPaletteOpen = false
local boxPaletteScroll = { scroll = 0 } local boxPaletteScroll = { scroll = 0 }
local boxPaletteSections = {} local boxPaletteSections = {}
-- Adhesion applied to any moveable block placed from the room view. "regular"
-- means no adhesion; the others attach as a per-instance property (see
-- Room:createEntity). `adhesionRect` is recomputed each frame in
-- layoutRoomControls so draw and hit-testing agree; the menu expands upward.
local adhesionOptions = { "regular", "sticky", "gluey" }
local selectedAdhesion = "regular"
local adhesionMenuOpen = false
local adhesionRect = nil
-- Note authoring state. `noteMode` is nil normally, "create" while placing a new -- Note authoring state. `noteMode` is nil normally, "create" while placing a new
-- note (click a cell to drop it and finish), or "edit" while retyping an -- note (click a cell to drop it and finish), or "edit" while retyping an
-- existing note (with Save / Discard). `editingNote` is the note being edited. -- existing note (with Save / Discard). `editingNote` is the note being edited.
@ -87,7 +96,10 @@ end
local function paintRoomCell(cell, button) local function paintRoomCell(cell, button)
if button == 1 and selectedAsset then if button == 1 and selectedAsset then
currentRoom:attemptPlaceAtCell(selectedAsset, gameAssets[selectedAsset], cell, selectedColor) -- adhesion is a no-op on non-moveable assets (Room:createEntity only reads
-- it in the moveable branch), so it is always safe to pass along.
local adhesion = selectedAdhesion ~= "regular" and selectedAdhesion or nil
currentRoom:attemptPlaceAtCell(selectedAsset, gameAssets[selectedAsset], cell, selectedColor, nil, adhesion)
elseif button == 2 then elseif button == 2 then
currentRoom:attemptDeleteAtCell(cell, selectedColor) currentRoom:attemptDeleteAtCell(cell, selectedColor)
end end
@ -132,16 +144,16 @@ local function currentRoomList()
end end
local function rebuildBoxPaletteSections() local function rebuildBoxPaletteSections()
-- Adhesion (sticky/gluey) is now a placement modifier chosen with the dropdown
-- next to the colour picker, not a separate asset, so the palette lists only
-- the base box shapes (plus the barrier).
boxPaletteSections = {} boxPaletteSections = {}
for _, prefix in ipairs({ "box", "spiky", "sticky", "skewer" }) do
local names = {} local names = {}
local pattern = prefix == "box" and "^box" or "^" .. prefix .. "_box"
for name in pairs(gameAssets) do for name in pairs(gameAssets) do
if name:match(pattern) then table.insert(names, name) end if name:match("^box") then table.insert(names, name) end
end end
table.sort(names) table.sort(names)
table.insert(boxPaletteSections, { prefix = prefix, names = names }) table.insert(boxPaletteSections, { prefix = "box", names = names })
end
if gameAssets.player_blocker then if gameAssets.player_blocker then
table.insert(boxPaletteSections, { prefix = "barrier", names = {"player_blocker"} }) table.insert(boxPaletteSections, { prefix = "barrier", names = {"player_blocker"} })
end end
@ -213,17 +225,9 @@ local function drawPaletteAsset(name, x, y, size)
for color, path in pairs(props.sprites) do for color, path in pairs(props.sprites) do
local sprite = loadSprite(path) local sprite = loadSprite(path)
love.graphics.setColor(gColor[color]:set()) love.graphics.setColor(gColor[color]:set())
if props.goopy and drawGoopySprite then
drawGoopySprite(sprite, drawX, drawY, scale)
elseif props.skewer and drawSkewerSprite then
drawSkewerSprite(sprite, props.cells and props.cells[color], drawX, drawY, scale)
elseif props.spiky and drawSpikySprite then
drawSpikySprite(sprite, props.cells and props.cells[color], drawX, drawY, scale)
else
love.graphics.draw(sprite, drawX, drawY, 0, scale, scale) love.graphics.draw(sprite, drawX, drawY, 0, scale, scale)
end end
end end
end
local function drawBoxPicker(picker) local function drawBoxPicker(picker)
Layout.clip(picker.rect) Layout.clip(picker.rect)
@ -332,11 +336,89 @@ local function layoutRoomControls(c, s)
y = y + target + vgap y = y + target + vgap
end end
placeRow({ "white", "red", "green", "blue" }, 22) -- Colour swatches with the adhesion dropdown packed to their right, centered
-- as one group. adhesionRect feeds both the dropdown draw and its hit-test.
do
local target = 22
local dropdownW = 78
local btns, total = {}, 0
for _, name in ipairs({ "white", "red", "green", "blue" }) do
local b = sizeBtn(name, target)
if b then table.insert(btns, b); total = total + b.w end
end
total = total + hgap * #btns + dropdownW
local x = c.x + (c.w - total) / 2
for _, b in ipairs(btns) do
b.x = x
b.y = y + (target - b.h) / 2
x = x + b.w + hgap
end
adhesionRect = { x = x, y = y, w = dropdownW, h = target }
y = y + target + vgap
end
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22) placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22)
placeRow({ "save", "clearSaves", "world", "wire", "note", "mark" }, 28) placeRow({ "save", "clearSaves", "world", "wire", "note", "mark" }, 28)
end end
-- Stacked option rows for the open adhesion menu, expanding upward from the
-- closed control so it never spills past the bottom edge. Shared by draw + click.
local function adhesionMenuRects()
if not adhesionRect then return {} end
local rects = {}
for index, name in ipairs(adhesionOptions) do
-- topmost option is the last one drawn highest above the closed control
local offset = (#adhesionOptions - index + 1) * adhesionRect.h
rects[index] = {
name = name,
x = adhesionRect.x,
y = adhesionRect.y - offset,
w = adhesionRect.w,
h = adhesionRect.h,
}
end
return rects
end
local function drawAdhesionDropdown()
if not adhesionRect then return end
local r = adhesionRect
love.graphics.setColor(gColor.white:set())
love.graphics.setLineWidth(2)
love.graphics.rectangle("line", r.x, r.y, r.w, r.h)
local caret = adhesionMenuOpen and " v" or " ^"
love.graphics.printf(selectedAdhesion:upper() .. caret, r.x, r.y + (r.h - font:getHeight()) / 2, r.w, "center")
if adhesionMenuOpen then
for _, option in ipairs(adhesionMenuRects()) do
love.graphics.setColor(0, 0, 0, 0.9)
love.graphics.rectangle("fill", option.x, option.y, option.w, option.h)
love.graphics.setColor(option.name == selectedAdhesion and 1 or 0.6, 0.85, option.name == selectedAdhesion and 0.2 or 0.6, 1)
love.graphics.rectangle("line", option.x, option.y, option.w, option.h)
love.graphics.setColor(gColor.white:set())
love.graphics.printf(option.name:upper(), option.x, option.y + (option.h - font:getHeight()) / 2, option.w, "center")
end
end
end
-- Returns true when the click was consumed by the dropdown (toggle or a choice).
local function adhesionDropdownClick(x, y)
if not adhesionRect then return false end
if adhesionMenuOpen then
for _, option in ipairs(adhesionMenuRects()) do
if Layout.hit(option, x, y) then
selectedAdhesion = option.name
adhesionMenuOpen = false
return true
end
end
end
if Layout.hit(adhesionRect, x, y) then
adhesionMenuOpen = not adhesionMenuOpen
return true
end
if adhesionMenuOpen then adhesionMenuOpen = false end
return false
end
-- Build the full room-view layout (sections + a scroll list + a back/search -- Build the full room-view layout (sections + a scroll list + a back/search
-- header) without drawing. Shared by editorDraw and editorMouseHandler. -- header) without drawing. Shared by editorDraw and editorMouseHandler.
local function buildRoomLayout() local function buildRoomLayout()
@ -402,10 +484,6 @@ 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,
spiky = gameAssets[asset].spiky,
cells = gameAssets[asset].cells,
name = "asset", name = "asset",
text = asset, text = asset,
x = 30, x = 30,
@ -739,10 +817,6 @@ function _initLevelEditor()
_addUIButton(createUIElement{ _addUIButton(createUIElement{
name = "quick_" .. asset, name = "quick_" .. asset,
sprites = sprites, sprites = sprites,
goopy = properties.goopy,
skewer = properties.skewer,
spiky = properties.spiky,
cells = properties.cells,
onClic = { left = function() selectQuickAsset(asset) end }, onClic = { left = function() selectQuickAsset(asset) end },
x = 0, x = 0,
y = 0, y = 0,
@ -819,10 +893,6 @@ 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,
spiky = gameAssets[asset].spiky,
cells = gameAssets[asset].cells,
name = "asset", name = "asset",
text = asset, text = asset,
x = 30, x = 30,
@ -1147,10 +1217,6 @@ function editAsset(asset)
local assetButton = createUIElement{ local assetButton = createUIElement{
name = asset, name = asset,
sprites = assetSprites, sprites = assetSprites,
goopy = assetProperties.goopy,
skewer = assetProperties.skewer,
spiky = assetProperties.spiky,
cells = assetProperties.cells,
x = sb.w / 2 - 18, x = sb.w / 2 - 18,
y = 80, y = 80,
} }
@ -1517,6 +1583,10 @@ function editorMouseHandler(x, y, button)
end end
end end
-- adhesion dropdown (sits next to the colour picker; its open menu
-- overlaps the area above the controls, so test it before the buttons)
if button == 1 and adhesionDropdownClick(x, y) then return end
-- control cluster (only the buttons that live there) -- control cluster (only the buttons that live there)
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note", "mark" }) do for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire", "note", "mark" }) do
local b = uiButtons[name] local b = uiButtons[name]
@ -1833,6 +1903,9 @@ function editorDraw()
end end
end end
-- adhesion dropdown, drawn last so its open menu sits above the controls
drawAdhesionDropdown()
-- status strip between the list and the controls: Save / Discard while -- status strip between the list and the controls: Save / Discard while
-- editing an existing note, otherwise the ordinary status line. -- editing an existing note, otherwise the ordinary status line.
if noteMode == "edit" then if noteMode == "edit" then

View file

@ -9,10 +9,6 @@ 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.spiky = t.spiky or false
self.cells = t.cells
if not t.onClic then if not t.onClic then
t.onClic = {} t.onClic = {}
@ -103,17 +99,9 @@ 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())
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)
elseif self.spiky and drawSpikySprite then
drawSpikySprite(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) love.graphics.draw(sprite, box.x, box.y, 0, self.scale, self.scale)
end end
end end
end
if self.text then if self.text then
love.graphics.setColor(gColor.white:set()) love.graphics.setColor(gColor.white:set())

View file

@ -1,7 +1,5 @@
require "libs/TSerial" require "libs/TSerial"
local BoxVariants = require "box_variants"
local targetFolder = "art" local targetFolder = "art"
local reanalyzeCells = true local reanalyzeCells = true
@ -77,5 +75,5 @@ function getAllAssets()
synthetic("wired_switch", "switch", "wired_switch") synthetic("wired_switch", "switch", "wired_switch")
synthetic("wired_door", "door", "wired_door") synthetic("wired_door", "door", "wired_door")
return BoxVariants.addTo(globalAssetProperties) return globalAssetProperties
end end

View file

@ -217,12 +217,10 @@ function MapManager:drawRoomLayer(roomData, x, y, size, color, alpha)
love.graphics.setColor(r, g, b, alpha) love.graphics.setColor(r, g, b, alpha)
local sprite = loadSprite(props.sprites[color]) local sprite = loadSprite(props.sprites[color])
local dx, dy = x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale local dx, dy = x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale
if props.goopy and drawGoopySprite then if object.adhesion == "gluey" and drawGlueySprite then
drawGlueySprite(sprite, props.cells and props.cells[color], dx, dy, scale)
elseif object.adhesion == "sticky" and drawGoopySprite then
drawGoopySprite(sprite, dx, dy, scale) drawGoopySprite(sprite, dx, dy, scale)
elseif props.skewer and drawSkewerSprite then
drawSkewerSprite(sprite, props.cells and props.cells[color], dx, dy, scale)
elseif props.spiky and drawSpikySprite then
drawSpikySprite(sprite, props.cells and props.cells[color], dx, dy, scale)
else else
love.graphics.draw(sprite, dx, dy, 0, scale, scale) love.graphics.draw(sprite, dx, dy, 0, scale, scale)
end end

View file

@ -1,6 +1,7 @@
-- 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 softly grabbed by sticky/spiky, or touches a rigid skewer. -- pushing), is softly grabbed by a sticky block, or is rigidly welded to a
-- gluey block.
local Movement = {} local Movement = {}
local adjacentDirections = { local adjacentDirections = {
@ -18,15 +19,11 @@ local function isPassable(entity)
return entity.canPassOver and entity:canPassOver() return entity.canPassOver and entity:canPassOver()
end end
local function isSkewer(entity) local function isGluey(entity)
return entity.isSkewer and entity:isSkewer() return entity.isGluey and entity:isGluey()
end end
local function isSpiky(entity) -- Build one rigid component: ordinary pushes are required, and gluey blocks weld
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 -- along every edge. `alreadyMoving` contains cells that will be vacated during
-- this same step, so a later pulled component may move into them. -- this same step, so a later pulled component may move into them.
local function findRigidGroup(startCells, direction, entityAt, isInBounds, alreadyMoving) local function findRigidGroup(startCells, direction, entityAt, isInBounds, alreadyMoving)
@ -59,7 +56,7 @@ local function findRigidGroup(startCells, direction, entityAt, isInBounds, alrea
table.insert(pendingCells, projectedCell) table.insert(pendingCells, projectedCell)
end end
-- Skewers make every edge contact part of this rigid component. -- Gluey blocks make every edge contact part of this rigid component.
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 = {
@ -69,7 +66,7 @@ local function findRigidGroup(startCells, direction, entityAt, isInBounds, alrea
if isInBounds(adjacentCell) then if isInBounds(adjacentCell) then
local adjacentEntity = entityAt(adjacentCell) local adjacentEntity = entityAt(adjacentCell)
local attached = adjacentEntity and ( local attached = adjacentEntity and (
isSkewer(entity) or isSkewer(adjacentEntity) isGluey(entity) or isGluey(adjacentEntity)
) )
if attached and adjacentEntity ~= entity and adjacentEntity:canMove() and if attached and adjacentEntity ~= entity and adjacentEntity:canMove() and
not (alreadyMoving and alreadyMoving[adjacentEntity]) then not (alreadyMoving and alreadyMoving[adjacentEntity]) then
@ -99,9 +96,9 @@ function Movement.findMovableGroup(startCells, direction, entityAt, isInBounds)
local entity = movingQueue[nextMover] local entity = movingQueue[nextMover]
nextMover = nextMover + 1 nextMover = nextMover + 1
-- Sticky contact is soft only on the trailing face. Spiky contact is soft -- Sticky contact is soft on every face: it tries to drag each neighbour
-- on every face. Either kind simply releases a blocked rigid component, -- along, but simply releases a blocked rigid component without cancelling
-- without cancelling this entity or another independently pulled branch. -- this entity or another independently pulled branch.
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 = {
@ -110,11 +107,8 @@ 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 softContact = adjacentEntity and ( local softContact = adjacentEntity and (
isSpiky(entity) or isSpiky(adjacentEntity) or isSticky(entity) or isSticky(adjacentEntity)
(trailing and (isSticky(entity) or isSticky(adjacentEntity)))
) )
if softContact and adjacentEntity ~= entity and if softContact and adjacentEntity ~= entity and
not entities[adjacentEntity] and not attemptedPulls[adjacentEntity] and not entities[adjacentEntity] and not attemptedPulls[adjacentEntity] and

View file

@ -12,9 +12,6 @@ require "wired_switch"
require "wired_door" require "wired_door"
require "dialogue_manager" require "dialogue_manager"
require "generic_moveable" require "generic_moveable"
require "generic_sticky"
require "generic_skewer"
require "generic_spiky"
require "generic_unmoveable" require "generic_unmoveable"
require "generic_player_blocker" require "generic_player_blocker"
require "note" require "note"
@ -772,6 +769,17 @@ function Room:createEntity(class, color, x, y, data)
return entity return entity
end end
-- Legacy: adhesion used to be baked into separate asset classes
-- (spiky_/skewer_/sticky_<box>). Collapse them to the base asset carrying an
-- adhesion string so old .sav files still load. spiky/old-sticky both become
-- the new soft "sticky"; skewer becomes the rigid "gluey".
local adhesion = data.adhesion
local legacyAdhesion = { spiky = "sticky", skewer = "gluey", sticky = "sticky" }
for prefix, mapped in pairs(legacyAdhesion) do
local base = class:match("^" .. prefix .. "_(.+)")
if base then class = base; adhesion = adhesion or mapped; break end
end
local props = globalAssetProperties and globalAssetProperties[class] local props = globalAssetProperties and globalAssetProperties[class]
local behavior = props and props.class or "immoveable" local behavior = props and props.class or "immoveable"
local entity local entity
@ -829,13 +837,7 @@ function Room:createEntity(class, color, x, y, data)
entity = GenericPlayerBlocker:new(x, y, color, sprite, cellShape) entity = GenericPlayerBlocker:new(x, y, color, sprite, cellShape)
self:registerPlayerBlocker(entity, color) self:registerPlayerBlocker(entity, color)
elseif behavior == "moveable" then elseif behavior == "moveable" then
entity = GenericMoveable:new(x, y, color, sprite, class, cellShape) entity = GenericMoveable:new(x, y, color, sprite, class, cellShape, adhesion)
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)
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
@ -1038,7 +1040,7 @@ function Room:attemptDelete(mouseX, mouseY, color)
return self:attemptDeleteAtCell(self:getCellFromMousePos(mouseX, mouseY), color) return self:attemptDeleteAtCell(self:getCellFromMousePos(mouseX, mouseY), color)
end end
function Room:attemptPlaceAtCell(name, props, cell, color, text) function Room:attemptPlaceAtCell(name, props, cell, color, text, adhesion)
-- Notes are colourless, single-instance annotations (no per-channel sprites), -- Notes are colourless, single-instance annotations (no per-channel sprites),
-- so they place once carrying their text rather than one copy per colour. One -- so they place once carrying their text rather than one copy per colour. One
-- note per cell: drop any note already here before adding the new one. -- note per cell: drop any note already here before adding the new one.
@ -1084,7 +1086,7 @@ function Room:attemptPlaceAtCell(name, props, cell, color, text)
-- additive blend combines the channels into full color -- additive blend combines the channels into full color
for _, c in ipairs(colors) do for _, c in ipairs(colors) do
if props.sprites[c] then if props.sprites[c] then
self:createEntity(name, c, cell.x, cell.y) self:createEntity(name, c, cell.x, cell.y, { adhesion = adhesion })
end end
end end
self:tick() self:tick()
@ -1181,6 +1183,7 @@ function Room:serialize(includePlayers)
local pos = entity:getGridPos() local pos = entity:getGridPos()
local object = { class = class, color = color, x = pos.x, y = pos.y } local object = { class = class, color = color, x = pos.x, y = pos.y }
if entity.circuitId then object.circuit = entity.circuitId end if entity.circuitId then object.circuit = entity.circuitId end
if entity.adhesion then object.adhesion = entity.adhesion end
table.insert(objects, object) table.insert(objects, object)
end end

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,598 +3,605 @@ wires={
}, },
objects={ objects={
{ {
color="red",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=2,
class="static_wall", class="static_wall",
y=2 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=3,
class="static_wall", class="static_wall",
y=3 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=4,
class="static_wall", class="static_wall",
y=4 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=5,
class="static_wall", class="static_wall",
y=5 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=5,
class="static_wall", class="static_wall",
y=5 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=6,
class="static_wall", class="static_wall",
y=6 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=7,
class="static_wall", class="static_wall",
y=7 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=8,
class="static_wall", class="static_wall",
y=8 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=9,
class="static_wall", class="static_wall",
y=9 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=10,
class="static_wall", class="static_wall",
y=10 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=4, x=4,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=9, x=9,
y=1,
class="box", class="box",
y=1 color="red"
}, },
{ {
x=7,
y=1,
adhesion="sticky",
color="red", color="red",
x=7, class="box_2x1_11"
class="spiky_box_2x1_11",
y=1
}, },
{ {
x=5,
y=1,
adhesion="sticky",
color="red", color="red",
x=5, class="box_2x1_11"
class="spiky_box_2x1_11",
y=1
}, },
{ {
color="green",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=2,
class="static_wall", class="static_wall",
y=2 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=3,
class="static_wall", class="static_wall",
y=3 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=4,
class="static_wall", class="static_wall",
y=4 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=5,
class="static_wall", class="static_wall",
y=5 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=5,
class="static_wall", class="static_wall",
y=5 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=6,
class="static_wall", class="static_wall",
y=6 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=7,
class="static_wall", class="static_wall",
y=7 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=8,
class="static_wall", class="static_wall",
y=8 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=9,
class="static_wall", class="static_wall",
y=9 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=10,
class="static_wall", class="static_wall",
y=10 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=4, x=4,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="blue",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=2,
class="static_wall", class="static_wall",
y=2 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=3,
class="static_wall", class="static_wall",
y=3 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=4,
class="static_wall", class="static_wall",
y=4 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=5,
class="static_wall", class="static_wall",
y=5 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=6,
class="static_wall", class="static_wall",
y=6 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=7,
class="static_wall", class="static_wall",
y=7 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=8,
class="static_wall", class="static_wall",
y=8 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=9,
class="static_wall", class="static_wall",
y=9 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=10,
class="static_wall", class="static_wall",
y=10 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=4, x=4,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=9, x=9,
class="spiky_box_2x1_11", y=10,
y=10 adhesion="sticky",
color="blue",
class="box_2x1_11"
}, },
{ {
color="blue",
x=7, x=7,
class="spiky_box_2x1_11", y=10,
y=10 adhesion="sticky",
color="blue",
class="box_2x1_11"
}, },
{ {
color="blue",
x=9, x=9,
class="spiky_box_2x1_11", y=9,
y=9 adhesion="sticky",
color="blue",
class="box_2x1_11"
}, },
{ {
color="blue",
x=8, x=8,
y=8,
class="box_2x2_11-10", class="box_2x2_11-10",
y=8 color="blue"
}, },
{ {
color="blue",
x=3, x=3,
class="spiky_box_2x1_11", y=10,
y=10 adhesion="sticky",
color="blue",
class="box_2x1_11"
}, },
{ {
color="blue",
x=5, x=5,
class="spiky_box_2x1_11", y=10,
y=10 adhesion="sticky",
color="blue",
class="box_2x1_11"
}, },
{ {
color="blue",
x=2, x=2,
y=9,
class="box_2x2_11-10", class="box_2x2_11-10",
y=9 color="blue"
} }
} }
} }

View file

@ -1,370 +1,383 @@
{ {
objects={ objects={
{ {
class="door",
color="blue",
x=6, x=6,
y=1 y=1,
color="blue",
class="door"
}, },
{ {
class="static_wall",
color="blue",
x=1, x=1,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=2, x=2,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=3, x=3,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=4, x=4,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=5, x=5,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=7, x=7,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=8, x=8,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=9, x=9,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=10, x=10,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=11, x=11,
y=1 y=1,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=1, x=1,
y=11 y=11,
color="blue",
class="static_wall"
}, },
{ {
class="static_wall",
color="blue",
x=11, x=11,
y=11 y=11,
color="blue",
class="static_wall"
}, },
{ {
class="spiky_box_2x1_11",
color="blue",
x=1, x=1,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="blue"
}, },
{ {
class="box",
color="blue",
x=3, x=3,
y=6 y=6,
color="blue",
class="box"
}, },
{ {
class="box_1x2_1-1",
color="blue",
x=4, x=4,
y=6 y=6,
color="blue",
class="box_1x2_1-1"
}, },
{ {
class="spiky_box_1x2_1-1",
color="blue",
x=5, x=5,
y=6 y=6,
adhesion="sticky",
class="box_1x2_1-1",
color="blue"
}, },
{ {
class="box_2x2_11-10",
color="blue",
x=6, x=6,
y=5 y=5,
color="blue",
class="box_2x2_11-10"
}, },
{ {
class="spiky_box_2x1_11",
color="blue",
x=7, x=7,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="blue"
}, },
{ {
class="spiky_box_2x1_11",
color="blue",
x=9, x=9,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="blue"
}, },
{ {
class="door",
color="red",
x=6, x=6,
y=1 y=1,
color="red",
class="door"
}, },
{ {
class="static_wall",
color="red",
x=1, x=1,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=2, x=2,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=3, x=3,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=4, x=4,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=5, x=5,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=7, x=7,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=8, x=8,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=9, x=9,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=10, x=10,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=11, x=11,
y=1 y=1,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=1, x=1,
y=11 y=11,
color="red",
class="static_wall"
}, },
{ {
class="static_wall",
color="red",
x=11, x=11,
y=11 y=11,
color="red",
class="static_wall"
}, },
{ {
class="spiky_box_2x1_11",
color="red",
x=1, x=1,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="red"
}, },
{ {
class="box",
color="red",
x=3, x=3,
y=6 y=6,
color="red",
class="box"
}, },
{ {
class="box_1x2_1-1",
color="red",
x=4, x=4,
y=6 y=6,
color="red",
class="box_1x2_1-1"
}, },
{ {
class="spiky_box_1x2_1-1",
color="red",
x=5, x=5,
y=6 y=6,
adhesion="sticky",
class="box_1x2_1-1",
color="red"
}, },
{ {
class="box_2x2_11-10",
color="red",
x=6, x=6,
y=5 y=5,
color="red",
class="box_2x2_11-10"
}, },
{ {
class="spiky_box_2x1_11",
color="red",
x=7, x=7,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="red"
}, },
{ {
class="spiky_box_2x1_11",
color="red",
x=9, x=9,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="red"
}, },
{ {
class="door",
color="green",
x=6, x=6,
y=1 y=1,
color="green",
class="door"
}, },
{ {
class="static_wall",
color="green",
x=1, x=1,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=2, x=2,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=3, x=3,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=4, x=4,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=5, x=5,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=7, x=7,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=8, x=8,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=9, x=9,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=10, x=10,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=11, x=11,
y=1 y=1,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=1, x=1,
y=11 y=11,
color="green",
class="static_wall"
}, },
{ {
class="static_wall",
color="green",
x=11, x=11,
y=11 y=11,
color="green",
class="static_wall"
}, },
{ {
class="spiky_box_2x1_11",
color="green",
x=3, x=3,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="green"
}, },
{ {
class="spiky_box_2x1_11",
color="green",
x=1, x=1,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="green"
}, },
{ {
class="box",
color="green",
x=5, x=5,
y=6 y=6,
color="green",
class="box"
}, },
{ {
class="box_1x2_1-1",
color="green",
x=6, x=6,
y=6 y=6,
color="green",
class="box_1x2_1-1"
}, },
{ {
class="spiky_box_1x2_1-1",
color="green",
x=7, x=7,
y=6 y=6,
adhesion="sticky",
class="box_1x2_1-1",
color="green"
}, },
{ {
class="box_2x2_11-10",
color="green",
x=8, x=8,
y=5 y=5,
color="green",
class="box_2x2_11-10"
}, },
{ {
class="spiky_box_2x1_11",
color="green",
x=9, x=9,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="green"
}, },
{ {
class="spiky_box_2x1_11",
color="green",
x=11, x=11,
y=6 y=6,
adhesion="sticky",
class="box_2x1_11",
color="green"
} }
} }
} }

View file

@ -3,585 +3,588 @@ wires={
}, },
objects={ objects={
{ {
color="red",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=2, x=2,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=3, x=3,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=4, x=4,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=5, x=5,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=6, x=6,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=7, x=7,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=8, x=8,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=9, x=9,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=10, x=10,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=5,
class="static_wall", class="static_wall",
y=5 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="red"
}, },
{ {
color="red",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=6, x=6,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="red"
}, },
{ {
color="red",
x=8, x=8,
y=4,
class="box", class="box",
y=4 color="red"
}, },
{ {
color="red",
x=7, x=7,
class="spiky_box_2x1_11", y=3,
y=3 adhesion="sticky",
color="red",
class="box_2x1_11"
}, },
{ {
color="red",
x=6, x=6,
class="spiky_box_2x1_11", y=4,
y=4 adhesion="sticky",
color="red",
class="box_2x1_11"
}, },
{ {
color="red",
x=4, x=4,
class="spiky_box_2x1_11", y=4,
y=4 adhesion="sticky",
color="red",
class="box_2x1_11"
}, },
{ {
color="red",
x=3, x=3,
y=3,
class="box_2x2_11-10", class="box_2x2_11-10",
y=3 color="red"
}, },
{ {
color="green",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=2, x=2,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=3, x=3,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=4, x=4,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=5, x=5,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=6, x=6,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=7, x=7,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=8, x=8,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=9, x=9,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=10, x=10,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=5,
class="static_wall", class="static_wall",
y=5 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="green"
}, },
{ {
color="green",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=6, x=6,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="green"
}, },
{ {
color="green",
x=4, x=4,
y=3,
class="box", class="box",
y=3 color="green"
}, },
{ {
color="blue",
x=1, x=1,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=2, x=2,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=3, x=3,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=4, x=4,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=5, x=5,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=6, x=6,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=7, x=7,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=8, x=8,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=9, x=9,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=10, x=10,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=1,
class="static_wall", class="static_wall",
y=1 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=2,
class="static_wall", class="static_wall",
y=2 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=3,
class="static_wall", class="static_wall",
y=3 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=4,
class="static_wall", class="static_wall",
y=4 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=5,
class="static_wall", class="static_wall",
y=5 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=6,
class="static_wall", class="static_wall",
y=6 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=7,
class="static_wall", class="static_wall",
y=7 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=8,
class="static_wall", class="static_wall",
y=8 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=9,
class="static_wall", class="static_wall",
y=9 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=10,
class="static_wall", class="static_wall",
y=10 color="blue"
}, },
{ {
color="blue",
x=1, x=1,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=2, x=2,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=3, x=3,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=5, x=5,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=6, x=6,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=7, x=7,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=8, x=8,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=9, x=9,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=10, x=10,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
color="blue",
x=11, x=11,
y=11,
class="static_wall", class="static_wall",
y=11 color="blue"
}, },
{ {
x=5, x=5,
class="npc", y=2,
y=2 class="npc"
} }
} }
} }

View file

@ -183,6 +183,64 @@ function drawGoopySprite(sprite, x, y, scale)
love.graphics.setShader(previousShader) love.graphics.setShader(previousShader)
end end
-- Gluey blocks draw teeth on every *true* edge of their collision shape: a cell
-- edge only gets spikes when no sibling cell sits on the other side of it, so an
-- L/notched shape spikes its real contour rather than its bounding box.
local glueyEdgeDirections = {
{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 drawSpikes(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(glueyEdgeDirections) 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
end
function drawGlueySprite(sprite, cellShape, x, y, scale)
drawSpikes(cellShape, x, y, scale)
love.graphics.draw(sprite, x, y, 0, scale, scale)
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

View file

@ -29,19 +29,16 @@ 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, spiky) local function movable(x, y, adhesion, shape)
local entity = { local entity = {
x = x, y = y, x = x, y = y,
sticky = sticky or false, adhesion = adhesion,
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.adhesion == "sticky" end
function entity:isSkewer() return self.skewer end function entity:isGluey() return self.adhesion == "gluey" 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
@ -94,7 +91,7 @@ end)
test("a trailing sticky block joins a horizontal pull", function() test("a trailing sticky block joins a horizontal pull", function()
-- sticky, regular, player; moving right -- sticky, regular, player; moving right
local sticky = movable(2, 3, true) local sticky = movable(2, 3, "sticky")
local regular = movable(3, 3) local regular = movable(3, 3)
local possible, group = plan({sticky, regular}, {x = 3, y = 3}, {x = 1, y = 0}) local possible, group = plan({sticky, regular}, {x = 3, y = 3}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
@ -105,7 +102,7 @@ end)
test("a moving sticky block 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, "sticky")
local possible, group = plan({regular, sticky}, {x = 3, y = 3}, {x = 1, y = 0}) local possible, group = plan({regular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
assertIn(group, sticky) assertIn(group, sticky)
@ -114,7 +111,7 @@ end)
test("sticky pull chains work vertically", function() test("sticky pull chains work vertically", function()
-- player moves down, pulling the two blocks above it -- player moves down, pulling the two blocks above it
local sticky = movable(4, 2, true) local sticky = movable(4, 2, "sticky")
local regular = movable(4, 3) local regular = movable(4, 3)
local possible, group = plan({sticky, regular}, {x = 4, y = 3}, {x = 0, y = 1}) local possible, group = plan({sticky, regular}, {x = 4, y = 3}, {x = 0, y = 1})
assertTrue(possible) assertTrue(possible)
@ -129,7 +126,7 @@ test("sticky pulls are symmetric in all four directions", function()
}) do }) do
local player = {x = 4, y = 4} local player = {x = 4, y = 4}
local regular = movable(player.x - direction.x, player.y - direction.y) 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 sticky = movable(player.x - direction.x * 2, player.y - direction.y * 2, "sticky")
local possible, group = plan( local possible, group = plan(
{sticky, regular}, {sticky, regular},
{x = regular.x, y = regular.y}, {x = regular.x, y = regular.y},
@ -141,19 +138,20 @@ test("sticky pulls are symmetric in all four directions", function()
end end
end) end)
test("a sticky block shears off during orthogonal movement", function() test("a sticky block grabs a free lateral block during orthogonal movement", function()
-- The regular block moves right; the sticky block above it stays behind. -- The regular block moves right; the sticky block above it is soft-grabbed on
-- that lateral face (sticky pulls on every edge) and follows along.
local regular = movable(3, 4) local regular = movable(3, 4)
local stickyBranch = movable(3, 3, true) local stickyBranch = movable(3, 3, "sticky")
local possible, group = plan({regular, stickyBranch}, {x = 3, y = 4}, {x = 1, y = 0}) local possible, group = plan({regular, stickyBranch}, {x = 3, y = 4}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
assertIn(group, regular) assertIn(group, regular)
assertNotIn(group, stickyBranch) assertIn(group, stickyBranch)
end) end)
test("a blocked sheared sticky block does not stop movement", function() test("a blocked sticky lateral grab does not stop movement", function()
local regular = movable(3, 4) local regular = movable(3, 4)
local stickyBranch = movable(3, 3, true) local stickyBranch = movable(3, 3, "sticky")
local blockingWall = wall(4, 3) local blockingWall = wall(4, 3)
local possible, group = plan({regular, stickyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0}) local possible, group = plan({regular, stickyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
@ -161,49 +159,49 @@ test("a blocked sheared sticky block does not stop movement", function()
assertNotIn(group, stickyBranch) assertNotIn(group, stickyBranch)
end) end)
test("a skewer carries an orthogonal branch", function() test("a gluey block welds an orthogonal branch", function()
local regular = movable(3, 4) local regular = movable(3, 4)
local skewerBranch = movable(3, 3, false, nil, true) local glueyBranch = movable(3, 3, "gluey")
local possible, group = plan({regular, skewerBranch}, {x = 3, y = 4}, {x = 1, y = 0}) local possible, group = plan({regular, glueyBranch}, {x = 3, y = 4}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
assertIn(group, regular) assertIn(group, regular)
assertIn(group, skewerBranch) assertIn(group, glueyBranch)
end) end)
test("a blocked skewer branch stops its rigid movement group", function() test("a blocked gluey branch stops its rigid movement group", function()
local regular = movable(3, 4) local regular = movable(3, 4)
local skewerBranch = movable(3, 3, false, nil, true) local glueyBranch = movable(3, 3, "gluey")
local blockingWall = wall(4, 3) local blockingWall = wall(4, 3)
local possible = plan({regular, skewerBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0}) local possible = plan({regular, glueyBranch, blockingWall}, {x = 3, y = 4}, {x = 1, y = 0})
assertFalse(possible) assertFalse(possible)
end) end)
test("a spiky block pulls both orthogonal side blocks", function() test("a sticky block pulls both orthogonal side blocks", function()
local spiky = movable(4, 4, false, nil, false, true) local sticky = movable(4, 4, "sticky")
local upper = movable(4, 3) local upper = movable(4, 3)
local lower = movable(4, 5) local lower = movable(4, 5)
local possible, group = plan( local possible, group = plan(
{spiky, upper, lower}, {x = 4, y = 4}, {x = 1, y = 0} {sticky, upper, lower}, {x = 4, y = 4}, {x = 1, y = 0}
) )
assertTrue(possible) assertTrue(possible)
assertIn(group, spiky) assertIn(group, sticky)
assertIn(group, upper) assertIn(group, upper)
assertIn(group, lower) assertIn(group, lower)
end) end)
test("a spiky block releases one blocked side while pulling the other", function() test("a sticky block releases one blocked side while pulling the other", function()
local spiky = movable(4, 4, false, nil, false, true) local sticky = movable(4, 4, "sticky")
local blockedUpper = movable(4, 3) local blockedUpper = movable(4, 3)
local freeLower = movable(4, 5) local freeLower = movable(4, 5)
local blockingWall = wall(5, 3) local blockingWall = wall(5, 3)
local possible, group = plan( local possible, group = plan(
{spiky, blockedUpper, freeLower, blockingWall}, {sticky, blockedUpper, freeLower, blockingWall},
{x = 4, y = 4}, {x = 1, y = 0} {x = 4, y = 4}, {x = 1, y = 0}
) )
assertTrue(possible) assertTrue(possible)
assertIn(group, spiky) assertIn(group, sticky)
assertNotIn(group, blockedUpper) assertNotIn(group, blockedUpper)
assertIn(group, freeLower) assertIn(group, freeLower)
end) end)
@ -219,16 +217,16 @@ end)
test("diagonal blocks do not stick", function() test("diagonal blocks do not stick", function()
local regular = movable(3, 3) local regular = movable(3, 3)
local diagonalSticky = movable(2, 2, true) local diagonalSticky = movable(2, 2, "sticky")
local possible, group = plan({regular, diagonalSticky}, {x = 3, y = 3}, {x = 1, y = 0}) local possible, group = plan({regular, diagonalSticky}, {x = 3, y = 3}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
assertIn(group, regular) assertIn(group, regular)
assertNotIn(group, diagonalSticky) assertNotIn(group, diagonalSticky)
end) end)
test("multi-cell sticky blocks pull from every trailing occupied cell", function() test("multi-cell sticky blocks pull from every occupied cell", function()
local tallRegular = movable(3, 3, false, {{x = 0, y = 0}, {x = 0, y = 1}}) local tallRegular = movable(3, 3, nil, {{x = 0, y = 0}, {x = 0, y = 1}})
local sticky = movable(2, 4, true) local sticky = movable(2, 4, "sticky")
local possible, group = plan({tallRegular, sticky}, {x = 3, y = 3}, {x = 1, y = 0}) local possible, group = plan({tallRegular, sticky}, {x = 3, y = 3}, {x = 1, y = 0})
assertTrue(possible) assertTrue(possible)
assertIn(group, tallRegular) assertIn(group, tallRegular)
@ -245,7 +243,7 @@ test("an L-shaped sticky pulls from its trailing face in every direction", funct
} }
for _, case in ipairs(cases) do for _, case in ipairs(cases) do
local stickyL = movable(3, 3, true, lShape) local stickyL = movable(3, 3, "sticky", lShape)
local trailing = movable(case.trailing.x, case.trailing.y) local trailing = movable(case.trailing.x, case.trailing.y)
local possible, group = plan( local possible, group = plan(
{stickyL, trailing}, {x = 3, y = 3}, case.direction {stickyL, trailing}, {x = 3, y = 3}, case.direction
@ -259,10 +257,10 @@ end)
test("an L-shaped sticky can release one blocked pull while completing another", function() 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 -- 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. -- upper object is pinned against a wall; the lower one is free to follow.
local stickyL = movable(3, 3, true, { local stickyL = movable(3, 3, "sticky", {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1}, {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 pinned = movable(2, 2, nil, {{x = 0, y = 0}, {x = 0, y = 1}})
local free = movable(2, 4) local free = movable(2, 4)
local blockingWall = wall(3, 2) local blockingWall = wall(3, 2)
local possible, group = plan( local possible, group = plan(
@ -278,7 +276,7 @@ end)
test("a T-shaped sticky pulls independently across a notched trailing face", function() 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. -- Moving up, three exposed lower faces pull into cells vacated by the T.
local stickyT = movable(3, 3, true, { local stickyT = movable(3, 3, "sticky", {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 2, y = 0}, {x = 0, y = 0}, {x = 1, y = 0}, {x = 2, y = 0},
{x = 1, y = 1}, {x = 1, y = 1},
}) })
@ -297,54 +295,46 @@ test("a T-shaped sticky pulls independently across a notched trailing face", fun
assertIn(group, trailingStem) assertIn(group, trailingStem)
end) end)
test("an L-shaped spiky block grabs moveables across several exposed faces", function() test("an L-shaped sticky block grabs moveables across several exposed faces", function()
local spikyL = movable(3, 3, false, { local stickyL = movable(3, 3, "sticky", {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1}, {x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
}, false, true) })
local above = movable(3, 2) local above = movable(3, 2)
local below = movable(3, 5) local below = movable(3, 5)
local behind = movable(2, 3) local behind = movable(2, 3)
local possible, group = plan( local possible, group = plan(
{spikyL, above, below, behind}, {stickyL, above, below, behind},
{x = 3, y = 3}, {x = 1, y = 0} {x = 3, y = 3}, {x = 1, y = 0}
) )
assertTrue(possible) assertTrue(possible)
assertIn(group, spikyL) assertIn(group, stickyL)
assertIn(group, above) assertIn(group, above)
assertIn(group, below) assertIn(group, below)
assertIn(group, behind) assertIn(group, behind)
end) end)
test("production sticky, skewer, and spiky blocks advertise distinct behavior", function() test("production moveables advertise adhesion (regular, sticky, gluey)", 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
function Color:getname() return self.name end function Color:getname() return self.name end
require "generic_moveable" require "generic_moveable"
require "generic_sticky"
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 = GenericMoveable:new(3, 2, "red", sprite, "box", nil, "sticky")
local skewer = GenericSkewer:new(4, 2, "red", sprite, "skewer_box") local gluey = GenericMoveable:new(4, 2, "red", sprite, "box", nil, "gluey")
local spiky = GenericSpiky:new(5, 2, "red", sprite, "spiky_box")
assertTrue(regular:canMove()) assertTrue(regular:canMove())
assertFalse(regular:isSticky()) assertFalse(regular:isSticky())
assertFalse(regular:isGluey())
assertTrue(sticky:canMove()) assertTrue(sticky:canMove())
assertTrue(sticky:isSticky()) assertTrue(sticky:isSticky())
assertFalse(sticky:isSkewer()) assertFalse(sticky:isGluey())
assertTrue(skewer:canMove()) assertTrue(gluey:canMove())
assertFalse(skewer:isSticky()) assertFalse(gluey:isSticky())
assertTrue(skewer:isSkewer()) assertTrue(gluey:isGluey())
assertFalse(skewer:isSpiky())
assertTrue(spiky:canMove())
assertFalse(spiky:isSticky())
assertFalse(spiky:isSkewer())
assertTrue(spiky:isSpiky())
end) end)
local function loadRoomClass() local function loadRoomClass()
@ -442,7 +432,7 @@ end)
test("Room applies the sticky-regular-player pull", function() test("Room applies the sticky-regular-player pull", function()
loadRoomClass() loadRoomClass()
local sticky = movable(2, 3, true) local sticky = movable(2, 3, "sticky")
local regular = movable(3, 3) local regular = movable(3, 3)
local player = movable(4, 3) local player = movable(4, 3)
local room = roomWith(player, {sticky, regular}) local room = roomWith(player, {sticky, regular})
@ -462,8 +452,8 @@ test("Room pulls sticky out of a pinned concave moveable", function()
-- mm_ mm_ -- mm_ mm_
local player = movable(1, 2) local player = movable(1, 2)
local wall = wall(2, 2) local wall = wall(2, 2)
local sticky = movable(1, 3, true) local sticky = movable(1, 3, "sticky")
local concave = movable(1, 3, false, { local concave = movable(1, 3, nil, {
{x = 1, y = 0}, {x = 1, y = 0},
{x = 0, y = 1}, {x = 1, y = 1}, {x = 0, y = 1}, {x = 1, y = 1},
}) })
@ -480,7 +470,7 @@ end)
test("Room moves an L-shaped sticky and both available trailing pulls", function() test("Room moves an L-shaped sticky and both available trailing pulls", function()
loadRoomClass() loadRoomClass()
local player = movable(3, 2) local player = movable(3, 2)
local stickyL = movable(3, 3, true, { local stickyL = movable(3, 3, "sticky", {
{x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1}, {x = 0, y = 0}, {x = 1, y = 0}, {x = 0, y = 1},
}) })
local trailingLeft = movable(3, 5) local trailingLeft = movable(3, 5)
@ -495,27 +485,13 @@ test("Room moves an L-shaped sticky and both available trailing pulls", function
assertTrue(trailingRight.x == 4 and trailingRight.y == 3) assertTrue(trailingRight.x == 4 and trailingRight.y == 3)
end) 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() test("Room never moves a component found by push and pull twice", function()
loadRoomClass() loadRoomClass()
local player = movable(4, 4) local player = movable(4, 4)
local front = movable(5, 4) local front = movable(5, 4)
local bridgeRight = movable(5, 3, false, nil, true) local bridgeRight = movable(5, 3, "gluey")
local bridgeMiddle = movable(4, 3, false, nil, true) local bridgeMiddle = movable(4, 3, "gluey")
local bridgeLeft = movable(3, 3, false, nil, true) local bridgeLeft = movable(3, 3, "gluey")
local rear = movable(3, 4) local rear = movable(3, 4)
local component = {front, bridgeRight, bridgeMiddle, bridgeLeft, rear} local component = {front, bridgeRight, bridgeMiddle, bridgeLeft, rear}
local room = roomWith(player, component) local room = roomWith(player, component)
@ -533,74 +509,53 @@ test("Room never moves a component found by push and pull twice", function()
assertTrue(player.x == 5) assertTrue(player.x == 5)
end) end)
test("Room pushes spiky onward while a blocked side shears off", function() test("Room pushes sticky onward while a blocked side shears off", function()
loadRoomClass() loadRoomClass()
local player = movable(3, 4) local player = movable(3, 4)
local spiky = movable(4, 4, false, nil, false, true) local sticky = movable(4, 4, "sticky")
local blockedUpper = movable(4, 3) local blockedUpper = movable(4, 3)
local freeLower = movable(4, 5) local freeLower = movable(4, 5)
local blockingWall = wall(5, 3) local blockingWall = wall(5, 3)
local room = roomWith(player, {spiky, blockedUpper, freeLower, blockingWall}) local room = roomWith(player, {sticky, blockedUpper, freeLower, blockingWall})
room:movePlayer("right") room:movePlayer("right")
assertTrue(player.x == 4 and player.y == 4) assertTrue(player.x == 4 and player.y == 4)
assertTrue(spiky.x == 5 and spiky.y == 4) assertTrue(sticky.x == 5 and sticky.y == 4)
assertTrue(blockedUpper.x == 4 and blockedUpper.y == 3) assertTrue(blockedUpper.x == 4 and blockedUpper.y == 3)
assertTrue(freeLower.x == 5 and freeLower.y == 5) assertTrue(freeLower.x == 5 and freeLower.y == 5)
assertTrue(blockingWall.x == 5 and blockingWall.y == 3) assertTrue(blockingWall.x == 5 and blockingWall.y == 3)
end) end)
test("every moveable base box gets all three synthetic behavior variants", function() test("legacy box-variant classes load as adhesion on a plain moveable", function()
local BoxVariants = require "box_variants" loadRoomClass()
local boxSprites = {red = "box_r.png"} local oldAssets, oldLoadSprite = globalAssetProperties, loadSprite
local boxCells = {red = {{x = 1, y = 1}}} local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
local shapeSprites = {blue = "box_shape_b.png"} globalAssetProperties = {
local shapeCells = {blue = {{x = 1, y = 1}, {x = 2, y = 1}}} box = { class = "moveable", sprites = {red = "box_r.png"}, cells = {red = {{x = 1, 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 = {
box = {class = "moveable", sprites = boxSprites, cells = boxCells},
["box_2x1_11"] = {class = "moveable", sprites = shapeSprites, cells = shapeCells},
["box_2x2_10-11"] = {class = "moveable", sprites = lSprites, cells = lCells},
box_statue = {class = "immoveable", sprites = {}},
beer_mug = {class = "moveable", sprites = {}},
} }
loadSprite = function() return sprite end
local room = setmetatable({
collideables = {red = {}, green = {}, blue = {}},
playerBlockers = {red = {}, green = {}, blue = {}},
}, {__index = Room})
BoxVariants.addTo(assets) -- Old .sav files baked adhesion into the class name; loading must collapse them
-- onto the base box carrying the new adhesion property.
local fromSpiky = room:createEntity("spiky_box", "red", 2, 2)
local fromSkewer = room:createEntity("skewer_box", "red", 3, 2)
local fromSticky = room:createEntity("sticky_box", "red", 4, 2)
assertTrue(assets.sticky_box.class == "sticky") assertTrue(fromSpiky:isSticky(), "legacy spiky_ should load as sticky")
assertTrue(assets.sticky_box.directory == "sticky") assertTrue(fromSkewer:isGluey(), "legacy skewer_ should load as gluey")
assertTrue(assets.sticky_box.synthetic) assertTrue(fromSticky:isSticky(), "legacy sticky_ should load as sticky")
assertTrue(assets.sticky_box.goopy) assertTrue(fromSpiky.saveClass == "box", "class should normalize to the base box")
assertTrue(assets.sticky_box.sprites == boxSprites, "variant should share base sprites")
assertTrue(assets.sticky_box.cells == boxCells, "variant should share collision cells") -- An explicit adhesion in the object data wins and rides through on a plain box.
assertTrue(assets["sticky_box_2x1_11"].sprites == shapeSprites) local placed = room:createEntity("box", "red", 5, 2, { adhesion = "gluey" })
assertTrue(assets["sticky_box_2x1_11"].cells == shapeCells) assertTrue(placed:isGluey())
assertTrue(assets["sticky_box_2x2_10-11"].sprites == lSprites) assertTrue(placed.saveClass == "box")
assertTrue(assets["sticky_box_2x2_10-11"].cells == lCells) globalAssetProperties, loadSprite = oldAssets, oldLoadSprite
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)
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_beer_mug)
assertFalse(assets.skewer_box_statue)
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 tests passed: " .. passed)

View file

@ -0,0 +1,60 @@
-- One-off migration: rewrite rooms/*.sav from the old baked-in adhesion classes
-- (spiky_/skewer_/sticky_<box>) to the new model of a plain base class carrying
-- an `adhesion` property. spiky and old-sticky both become the new soft "sticky";
-- skewer becomes the rigid "gluey". Run once from the project root: lua tools/backfill_adhesion.lua
loadstring = loadstring or load -- TSerial.unpack still calls loadstring (removed in Lua 5.4+)
package.path = "./?.lua;" .. package.path
require "libs/TSerial"
local dir = "rooms"
local adhesionForPrefix = { spiky = "sticky", skewer = "gluey", sticky = "sticky" }
local function savFiles()
local files = {}
local pipe = io.popen('ls "' .. dir .. '"')
for name in pipe:lines() do
if name:match("%.sav$") then table.insert(files, name) end
end
pipe:close()
return files
end
local function readFile(path)
local handle = assert(io.open(path, "r"))
local contents = handle:read("*a")
handle:close()
return contents
end
local function writeFile(path, contents)
local handle = assert(io.open(path, "w"))
handle:write(contents)
handle:close()
end
local totalFiles, totalObjects = 0, 0
for _, file in ipairs(savFiles()) do
local path = dir .. "/" .. file
local saveTable = TSerial.unpack(readFile(path))
local migrated = 0
for _, object in ipairs((saveTable and saveTable.objects) or {}) do
if type(object.class) == "string" then
for prefix, adhesion in pairs(adhesionForPrefix) do
local base = object.class:match("^" .. prefix .. "_(.+)")
if base then
object.class = base
if not object.adhesion then object.adhesion = adhesion end
migrated = migrated + 1
break
end
end
end
end
if migrated > 0 then
writeFile(path, TSerial.pack(saveTable, nil, true))
totalFiles = totalFiles + 1
totalObjects = totalObjects + migrated
print(string.format("%-28s migrated %d object(s)", file, migrated))
end
end
print(string.format("done: %d object(s) across %d file(s)", totalObjects, totalFiles))

View file

@ -637,8 +637,7 @@ function World:movementContext(room, color)
function proxy:canMove() return self.entity:canMove() end function proxy:canMove() return self.entity:canMove() end
function proxy:canPassOver() return self.entity:canPassOver() end function proxy:canPassOver() return self.entity:canPassOver() end
function proxy:isSticky() return self.entity:isSticky() end function proxy:isSticky() return self.entity:isSticky() end
function proxy:isSkewer() return self.entity:isSkewer() end function proxy:isGluey() return self.entity:isGluey() end
function proxy:isSpiky() return self.entity:isSpiky() end
function proxy:bump() return self.entity:bump() end function proxy:bump() return self.entity:bump() end
function proxy:getOccupiedCells(offset) function proxy:getOccupiedCells(offset)
offset = offset or {x = 0, y = 0} offset = offset or {x = 0, y = 0}
@ -1110,14 +1109,13 @@ function World:drawRoomPreview(name, x, y, size, roomData)
if path then if path then
love.graphics.setColor(gColor[color]:set()) love.graphics.setColor(gColor[color]:set())
local sprite = loadSprite(path) local sprite = loadSprite(path)
if props.goopy and drawGoopySprite then local dx, dy = x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale) if object.adhesion == "gluey" and drawGlueySprite then
elseif props.skewer and drawSkewerSprite then drawGlueySprite(sprite, props.cells and props.cells[color], dx, dy, scale)
drawSkewerSprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale) elseif object.adhesion == "sticky" and drawGoopySprite then
elseif props.spiky and drawSpikySprite then drawGoopySprite(sprite, dx, dy, scale)
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, dx, dy, 0, scale, scale)
end end
end end
end end