chroma_solstice/docs/adhesion-refactor-plan.md

248 lines
12 KiB
Markdown
Raw Permalink Normal View History

# 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.