# 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_). 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 = , 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.