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
|
|
@ -63,6 +63,15 @@ local boxPaletteOpen = false
|
|||
local boxPaletteScroll = { scroll = 0 }
|
||||
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 (click a cell to drop it and finish), or "edit" while retyping an
|
||||
-- existing note (with Save / Discard). `editingNote` is the note being edited.
|
||||
|
|
@ -87,7 +96,10 @@ end
|
|||
|
||||
local function paintRoomCell(cell, button)
|
||||
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
|
||||
currentRoom:attemptDeleteAtCell(cell, selectedColor)
|
||||
end
|
||||
|
|
@ -132,16 +144,16 @@ local function currentRoomList()
|
|||
end
|
||||
|
||||
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 = {}
|
||||
for _, prefix in ipairs({ "box", "spiky", "sticky", "skewer" }) do
|
||||
local names = {}
|
||||
local pattern = prefix == "box" and "^box" or "^" .. prefix .. "_box"
|
||||
for name in pairs(gameAssets) do
|
||||
if name:match(pattern) then table.insert(names, name) end
|
||||
end
|
||||
table.sort(names)
|
||||
table.insert(boxPaletteSections, { prefix = prefix, names = names })
|
||||
local names = {}
|
||||
for name in pairs(gameAssets) do
|
||||
if name:match("^box") then table.insert(names, name) end
|
||||
end
|
||||
table.sort(names)
|
||||
table.insert(boxPaletteSections, { prefix = "box", names = names })
|
||||
if gameAssets.player_blocker then
|
||||
table.insert(boxPaletteSections, { prefix = "barrier", names = {"player_blocker"} })
|
||||
end
|
||||
|
|
@ -213,15 +225,7 @@ local function drawPaletteAsset(name, x, y, size)
|
|||
for color, path in pairs(props.sprites) do
|
||||
local sprite = loadSprite(path)
|
||||
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)
|
||||
end
|
||||
love.graphics.draw(sprite, drawX, drawY, 0, scale, scale)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -332,11 +336,89 @@ local function layoutRoomControls(c, s)
|
|||
y = y + target + vgap
|
||||
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({ "save", "clearSaves", "world", "wire", "note", "mark" }, 28)
|
||||
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
|
||||
-- header) without drawing. Shared by editorDraw and editorMouseHandler.
|
||||
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
|
||||
table.insert(assetSearchButtons, createUIElement{
|
||||
sprites = sprites,
|
||||
goopy = gameAssets[asset].goopy,
|
||||
skewer = gameAssets[asset].skewer,
|
||||
spiky = gameAssets[asset].spiky,
|
||||
cells = gameAssets[asset].cells,
|
||||
name = "asset",
|
||||
text = asset,
|
||||
x = 30,
|
||||
|
|
@ -739,10 +817,6 @@ function _initLevelEditor()
|
|||
_addUIButton(createUIElement{
|
||||
name = "quick_" .. asset,
|
||||
sprites = sprites,
|
||||
goopy = properties.goopy,
|
||||
skewer = properties.skewer,
|
||||
spiky = properties.spiky,
|
||||
cells = properties.cells,
|
||||
onClic = { left = function() selectQuickAsset(asset) end },
|
||||
x = 0,
|
||||
y = 0,
|
||||
|
|
@ -819,10 +893,6 @@ function selectFolder(folder)
|
|||
-- each frame from the list section (see buildRoomLayout).
|
||||
table.insert(assetListButtons, createUIElement{
|
||||
sprites = sprites,
|
||||
goopy = gameAssets[asset].goopy,
|
||||
skewer = gameAssets[asset].skewer,
|
||||
spiky = gameAssets[asset].spiky,
|
||||
cells = gameAssets[asset].cells,
|
||||
name = "asset",
|
||||
text = asset,
|
||||
x = 30,
|
||||
|
|
@ -1147,10 +1217,6 @@ function editAsset(asset)
|
|||
local assetButton = createUIElement{
|
||||
name = asset,
|
||||
sprites = assetSprites,
|
||||
goopy = assetProperties.goopy,
|
||||
skewer = assetProperties.skewer,
|
||||
spiky = assetProperties.spiky,
|
||||
cells = assetProperties.cells,
|
||||
x = sb.w / 2 - 18,
|
||||
y = 80,
|
||||
}
|
||||
|
|
@ -1517,6 +1583,10 @@ function editorMouseHandler(x, y, button)
|
|||
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)
|
||||
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]
|
||||
|
|
@ -1833,6 +1903,9 @@ function editorDraw()
|
|||
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
|
||||
-- editing an existing note, otherwise the ordinary status line.
|
||||
if noteMode == "edit" then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue