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:
parent
c348b81a93
commit
7d84f595e4
25 changed files with 4010 additions and 3797 deletions
247
docs/adhesion-refactor-plan.md
Normal file
247
docs/adhesion-refactor-plan.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue