wired switches and doors
This commit is contained in:
parent
ee7d36e72b
commit
bc4976b324
16 changed files with 2575 additions and 835 deletions
|
|
@ -44,6 +44,11 @@ local worldSearchText = ""
|
|||
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
|
||||
local cloneSourceRoom = nil
|
||||
local roomBrush = nil
|
||||
local wireBrush = nil
|
||||
local selectedCircuitId = nil
|
||||
local wireCameraX, wireCameraY = nil, nil
|
||||
local wireDirtyRooms = {}
|
||||
local wireOriginalWires = nil
|
||||
local boxPaletteOpen = false
|
||||
local boxPaletteScroll = { scroll = 0 }
|
||||
local boxPaletteSections = {}
|
||||
|
|
@ -273,6 +278,8 @@ local function layoutRoomControls(c, s)
|
|||
end
|
||||
b.scale = math.min(target / spriteW, target / spriteH)
|
||||
b.w, b.h = spriteW * b.scale, spriteH * b.scale
|
||||
else
|
||||
b.w, b.h = 72, target
|
||||
end
|
||||
return b
|
||||
end
|
||||
|
|
@ -296,8 +303,8 @@ local function layoutRoomControls(c, s)
|
|||
end
|
||||
|
||||
placeRow({ "white", "red", "green", "blue" }, 22)
|
||||
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box" }, 22)
|
||||
placeRow({ "save", "clearSaves", "world" }, 28)
|
||||
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22)
|
||||
placeRow({ "save", "clearSaves", "world", "wire" }, 28)
|
||||
end
|
||||
|
||||
-- Build the full room-view layout (sections + a scroll list + a back/search
|
||||
|
|
@ -497,6 +504,69 @@ local function saveRoomFromEditor()
|
|||
roomNameField:editText(name)
|
||||
activeField = nil
|
||||
editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or " without player positions.")
|
||||
|
||||
-- A circuit stroke can touch neighbouring rooms in one gesture. Save those
|
||||
-- authored fragments together so a cross-room route cannot be half-written.
|
||||
for room in pairs(wireDirtyRooms) do
|
||||
if room ~= currentRoom and room.name then
|
||||
room:saveWiresToSource()
|
||||
gameWorld:invalidatePreview(room.name)
|
||||
clearRuntimeSaveData(room.name)
|
||||
end
|
||||
end
|
||||
wireDirtyRooms = {}
|
||||
end
|
||||
|
||||
local function copyWires(wires)
|
||||
local copy = {}
|
||||
for y, row in pairs(wires or {}) do
|
||||
copy[y] = {}
|
||||
for x, tracks in pairs(row) do
|
||||
copy[y][x] = {}
|
||||
for circuitId, mask in pairs(tracks) do copy[y][x][circuitId] = mask end
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local function resumeWireRoom(status)
|
||||
local name = wireCameraX and wireCameraY and gameWorld:roomAt(wireCameraX, wireCameraY)
|
||||
local room = name and gameWorld.levels[name]
|
||||
if not room then
|
||||
room = currentRoom
|
||||
status = status .. " The centered world cell is empty, so the previous room remains open."
|
||||
end
|
||||
currentRoom = room
|
||||
gameWorld:setLevel(currentRoom)
|
||||
gameWorld:setLastRoom(currentRoom.name)
|
||||
setEditorRoomName(currentRoom.name)
|
||||
wireOriginalWires = nil
|
||||
wireDirtyRooms = {}
|
||||
assetsView()
|
||||
editorStatus = status
|
||||
end
|
||||
|
||||
local function saveWireRooms()
|
||||
local saved = 0
|
||||
for room in pairs(wireDirtyRooms) do
|
||||
local success = room == currentRoom and room:saveToSource(room.name) or room:saveWiresToSource()
|
||||
if room.name and success then
|
||||
saved = saved + 1
|
||||
gameWorld:invalidatePreview(room.name)
|
||||
clearRuntimeSaveData(room.name)
|
||||
end
|
||||
end
|
||||
suppressRuntimeAutosave = true
|
||||
local status = saved > 0 and ("Saved wire fragments in " .. saved .. " room(s).") or "No wire changes to save."
|
||||
resumeWireRoom(status)
|
||||
end
|
||||
|
||||
|
||||
local function discardWireRooms()
|
||||
for room, wires in pairs(wireOriginalWires or {}) do room.wires = copyWires(wires) end
|
||||
gameWorld:resolveCircuits()
|
||||
gameWorld:rebuildCollision()
|
||||
resumeWireRoom("Discarded wire changes.")
|
||||
end
|
||||
|
||||
local function clearRuntimeSaves()
|
||||
|
|
@ -631,7 +701,7 @@ function _initLevelEditor()
|
|||
}, color)
|
||||
end
|
||||
|
||||
for _, asset in ipairs({ "player", "static_wall", "switch", "door", "box" }) do
|
||||
for _, asset in ipairs({ "player", "static_wall", "switch", "door", "wired_switch", "wired_door", "box" }) do
|
||||
local properties = gameAssets[asset]
|
||||
if properties then
|
||||
local sprites = {}
|
||||
|
|
@ -650,6 +720,16 @@ function _initLevelEditor()
|
|||
end
|
||||
end
|
||||
|
||||
_addUIButton(createUIElement{
|
||||
name = "wire",
|
||||
text = "WIRES",
|
||||
onClic = { left = function()
|
||||
if editorView == "wire" then assetsView() else wireView() end
|
||||
end },
|
||||
x = 0,
|
||||
y = 0,
|
||||
}, "wire")
|
||||
|
||||
roomNameField = createUIElement{
|
||||
name = "roomName",
|
||||
text = editorRoomName,
|
||||
|
|
@ -839,11 +919,31 @@ end
|
|||
function assetsView()
|
||||
editingAssetButtons = {}
|
||||
editorView = "room"
|
||||
wireBrush = nil
|
||||
labels = {}
|
||||
end
|
||||
|
||||
function wireView()
|
||||
local location = currentRoom and gameWorld:roomLocation(currentRoom.name)
|
||||
if not location then
|
||||
editorStatus = "Place this room in the world before drawing cross-room wires."
|
||||
return
|
||||
end
|
||||
editorView = "wire"
|
||||
activeField = nil
|
||||
roomBrush = nil
|
||||
wireCameraX, wireCameraY = location.x, location.y
|
||||
wireOriginalWires = {}
|
||||
for _, room in pairs(gameWorld.levels or {}) do wireOriginalWires[room] = copyWires(room.wires) end
|
||||
wireDirtyRooms = {}
|
||||
editorStatus = selectedCircuitId
|
||||
and "Circuit selected. Drag to draw; right-drag erases; arrow keys pan."
|
||||
or "Click a wired door to select its circuit, then drag to its switches."
|
||||
end
|
||||
|
||||
function worldView()
|
||||
editorView = "world"
|
||||
wireBrush = nil
|
||||
activeField = nil
|
||||
selectedFolder = nil
|
||||
selectedRoom = nil
|
||||
|
|
@ -852,6 +952,118 @@ function worldView()
|
|||
rebuildWorldRoomButtons()
|
||||
end
|
||||
|
||||
local function wireControls()
|
||||
local pad, gap, h = 12, 8, 40
|
||||
local w = (sb.w - pad * 2 - gap) / 2
|
||||
return {
|
||||
save = {x = pad, y = sb.h - pad - h, w = w, h = h, text = "SAVE"},
|
||||
discard = {x = pad + w + gap, y = sb.h - pad - h, w = w, h = h, text = "DISCARD"},
|
||||
}
|
||||
end
|
||||
|
||||
local function rememberWireRooms(rooms)
|
||||
for _, room in ipairs(rooms or {}) do wireDirtyRooms[room] = true end
|
||||
end
|
||||
|
||||
local function wireCellAt(x, y)
|
||||
if x < 0 or x >= width or y < 0 or y >= height then return nil end
|
||||
if not wireCameraX or not wireCameraY then return nil end
|
||||
local cell = gameWorld:wireEditorCellAt(x, y, wireCameraX, wireCameraY)
|
||||
local room = gameWorld:roomCell(cell)
|
||||
return room and cell or nil
|
||||
end
|
||||
|
||||
local function paintWireStep(from, to, button)
|
||||
if button == 1 then
|
||||
local rooms, err = gameWorld:addWireConnection(from, to, selectedCircuitId)
|
||||
rememberWireRooms(rooms)
|
||||
if err then editorStatus = err end
|
||||
else
|
||||
rememberWireRooms(gameWorld:removeWireAt(to, selectedCircuitId))
|
||||
end
|
||||
end
|
||||
|
||||
local function paintWireLine(from, to, button)
|
||||
local current = {x = from.x, y = from.y}
|
||||
local function walk(axis, target)
|
||||
local step = current[axis] < target and 1 or -1
|
||||
while current[axis] ~= target do
|
||||
local nextCell = {x = current.x, y = current.y}
|
||||
nextCell[axis] = nextCell[axis] + step
|
||||
local room = gameWorld:roomCell(nextCell)
|
||||
if not room then return false end
|
||||
paintWireStep(current, nextCell, button)
|
||||
current = nextCell
|
||||
end
|
||||
return true
|
||||
end
|
||||
-- Manhattan interpolation keeps every authored segment orthogonal even if
|
||||
-- the mouse skips diagonally across several cells between move events.
|
||||
if not walk("x", to.x) then return current end
|
||||
walk("y", to.y)
|
||||
return current
|
||||
end
|
||||
|
||||
local function beginWireBrush(x, y, button)
|
||||
if button ~= 1 and button ~= 2 then return end
|
||||
local cell = wireCellAt(x, y)
|
||||
if not cell then return end
|
||||
if button == 1 then
|
||||
local door = gameWorld:wiredDoorAt(cell)
|
||||
if door then
|
||||
-- Each stroke leaving a door is its own run and may terminate at one
|
||||
-- logical switch. Multiple runs can meet at the door, whose state is the
|
||||
-- AND of those independently coloured switch terminals.
|
||||
selectedCircuitId = gameWorld:newCircuitId()
|
||||
editorStatus = "Started " .. selectedCircuitId .. ". End this run on one switch."
|
||||
else
|
||||
-- A wire is a selectable terminal even after its door is deleted.
|
||||
-- Clicking empty space keeps the current circuit so a new branch can
|
||||
-- still begin away from the existing route.
|
||||
local circuitAtCell = gameWorld:circuitAt(cell)
|
||||
if circuitAtCell then
|
||||
selectedCircuitId = circuitAtCell
|
||||
editorStatus = "Selected orphaned circuit " .. selectedCircuitId .. "."
|
||||
end
|
||||
end
|
||||
if not selectedCircuitId then
|
||||
editorStatus = "Start by clicking a wired door."
|
||||
return
|
||||
end
|
||||
rememberWireRooms(gameWorld:ensureWireAt(cell, selectedCircuitId))
|
||||
else
|
||||
-- Prefer the selected circuit when several routes overlap here; otherwise
|
||||
-- adopt the top route. Repeated right-clicks can therefore remove every
|
||||
-- orphan at an overlapping cell without needing a door.
|
||||
local circuitAtCell = gameWorld:circuitAt(cell, selectedCircuitId)
|
||||
if not circuitAtCell then
|
||||
editorStatus = "There is no wire at this cell to erase."
|
||||
return
|
||||
end
|
||||
selectedCircuitId = circuitAtCell
|
||||
rememberWireRooms(gameWorld:removeWireAt(cell, selectedCircuitId))
|
||||
editorStatus = "Erasing " .. selectedCircuitId .. "."
|
||||
end
|
||||
wireBrush = {button = button, lastCell = cell}
|
||||
end
|
||||
|
||||
function editorWireKeypressed(key)
|
||||
if editorView ~= "wire" then return false end
|
||||
local directions = {
|
||||
left = {-1, 0}, right = {1, 0}, up = {0, -1}, down = {0, 1},
|
||||
}
|
||||
local direction = directions[key]
|
||||
if not direction then return false end
|
||||
wireCameraX = constrain(wireCameraX + direction[1], 1, gameWorld.width)
|
||||
wireCameraY = constrain(wireCameraY + direction[2], 1, gameWorld.height)
|
||||
wireBrush = nil
|
||||
return true
|
||||
end
|
||||
|
||||
function wireEditorCamera()
|
||||
return wireCameraX, wireCameraY, selectedCircuitId
|
||||
end
|
||||
|
||||
local function newWorldRoomName(cellX, cellY)
|
||||
local base = string.format("room_%02d_%02d", cellX, cellY)
|
||||
local name, suffix = base, 2
|
||||
|
|
@ -872,6 +1084,23 @@ local function cloneWorldRoom(sourceName, cloneName)
|
|||
editorStatus = "A level named '" .. cloneName .. "' already exists."
|
||||
return false
|
||||
end
|
||||
-- A clone is a new puzzle slice, so remap every circuit identity found in
|
||||
-- it. Keeping the originals would let the clone silently join the source's
|
||||
-- cross-room wires when both are placed in the same world.
|
||||
local ok, saveTable = pcall(TSerial.unpack, source)
|
||||
if ok and type(saveTable) == "table" then
|
||||
local remapped = {}
|
||||
local function remap(circuitId)
|
||||
if not circuitId then return nil end
|
||||
if not remapped[circuitId] then remapped[circuitId] = gameWorld:newCircuitId() end
|
||||
return remapped[circuitId]
|
||||
end
|
||||
for _, object in ipairs(saveTable.objects or {}) do
|
||||
if object.circuit then object.circuit = remap(object.circuit) end
|
||||
end
|
||||
for _, wire in ipairs(saveTable.wires or {}) do wire.circuit = remap(wire.circuit) end
|
||||
source = TSerial.pack(saveTable, nil, true)
|
||||
end
|
||||
if not writeToSource("rooms/" .. cloneName .. ".sav", source) then
|
||||
editorStatus = "Could not create clone '" .. cloneName .. "'."
|
||||
return false
|
||||
|
|
@ -961,13 +1190,17 @@ function editorMouseHandler(x, y, button)
|
|||
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_box", "save", "clearSaves", "world" }) 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" }) do
|
||||
local b = uiButtons[name]
|
||||
if b and b:clicIsInBox(x, y) then
|
||||
b:onClic(button)
|
||||
return
|
||||
end
|
||||
end
|
||||
elseif editorView == "wire" then
|
||||
local controls = wireControls()
|
||||
if buttonContains(controls.save, x, y) then saveWireRooms(); return end
|
||||
if buttonContains(controls.discard, x, y) then discardWireRooms(); return end
|
||||
elseif editorView == "asset" then
|
||||
for _, uiElement in pairs(editingAssetButtons) do
|
||||
if uiElement:clicIsInBox(x, y) then
|
||||
|
|
@ -1005,6 +1238,8 @@ function editorMouseHandler(x, y, button)
|
|||
-- click landed on the grid (game area): place the selected asset
|
||||
if editorView == "room" then
|
||||
beginRoomBrush(x, y, button)
|
||||
elseif editorView == "wire" then
|
||||
beginWireBrush(x, y, button)
|
||||
elseif editorView == "world" then
|
||||
local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
|
||||
if not gameWorld:isInBounds(cellX, cellY) then return end
|
||||
|
|
@ -1027,6 +1262,15 @@ function editorMouseHandler(x, y, button)
|
|||
end
|
||||
|
||||
function editorMouseMoved(x, y)
|
||||
if wireBrush then
|
||||
if editorView ~= "wire" then wireBrush = nil; return end
|
||||
local cell = wireCellAt(x, y)
|
||||
if not cell then wireBrush = nil; return end
|
||||
if cell.x ~= wireBrush.lastCell.x or cell.y ~= wireBrush.lastCell.y then
|
||||
wireBrush.lastCell = paintWireLine(wireBrush.lastCell, cell, wireBrush.button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if not roomBrush then return end
|
||||
if editorView ~= "room" then
|
||||
roomBrush = nil
|
||||
|
|
@ -1048,6 +1292,7 @@ end
|
|||
|
||||
function editorMouseReleased(_, _, button)
|
||||
if roomBrush and roomBrush.button == button then roomBrush = nil end
|
||||
if wireBrush and wireBrush.button == button then wireBrush = nil end
|
||||
end
|
||||
|
||||
function editorTextHandler(text)
|
||||
|
|
@ -1097,6 +1342,7 @@ function editorCompleteResponse()
|
|||
end
|
||||
|
||||
function editorCancelResponse()
|
||||
if editorView == "wire" then discardWireRooms(); return true end
|
||||
if editorView ~= "world" or activeField ~= "cloneName" then return false end
|
||||
activeField = nil
|
||||
cloneSourceRoom = nil
|
||||
|
|
@ -1156,11 +1402,15 @@ function editorDraw()
|
|||
-- control cluster
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
|
||||
for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) 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" }) do
|
||||
local button = uiButtons[name]
|
||||
if button then
|
||||
button:draw()
|
||||
local asset = name:match("^quick_(.+)$")
|
||||
if button then
|
||||
button:draw()
|
||||
local asset = name:match("^quick_(.+)$")
|
||||
if asset and asset:match("^wired_") then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.print("W", button.x + button.w - 7, button.y - 4)
|
||||
end
|
||||
if asset and selectedAsset == asset then
|
||||
love.graphics.setColor(1, 0.85, 0.2, 1)
|
||||
love.graphics.setLineWidth(2)
|
||||
|
|
@ -1176,6 +1426,32 @@ function editorDraw()
|
|||
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
|
||||
Layout.unclip()
|
||||
end
|
||||
elseif editorView == "wire" then
|
||||
local circuitColor = selectedCircuitId and require("wire").color(selectedCircuitId)
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf("CIRCUIT WIRING", 10, 16, sb.w - 20, "center")
|
||||
if selectedCircuitId then
|
||||
love.graphics.setColor(circuitColor[1], circuitColor[2], circuitColor[3], 1)
|
||||
love.graphics.circle("fill", 28, 65, 9)
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(selectedCircuitId, 46, 54, sb.w - 58, "left")
|
||||
else
|
||||
love.graphics.printf("No circuit selected", 10, 54, sb.w - 20, "center")
|
||||
end
|
||||
love.graphics.printf(
|
||||
"Click a door to start a new run, or a wire to edit that run. Each run ends at one switch; start another run from the door for another switch.\n\nLeft-drag draws. Right-drag erases. Independent wires may share a cell without connecting.\n\nArrow keys pan by one room.",
|
||||
24, 100, sb.w - 48, "left"
|
||||
)
|
||||
if editorStatus then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(editorStatus, 24, sb.h * 0.55, sb.w - 48, "center")
|
||||
end
|
||||
local controls = wireControls()
|
||||
for _, control in pairs(controls) do
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.rectangle("line", control.x, control.y, control.w, control.h)
|
||||
love.graphics.printf(control.text, control.x, control.y + 11, control.w, "center")
|
||||
end
|
||||
elseif editorView == "asset" then
|
||||
for _, button in pairs(editingAssetButtons) do
|
||||
button:draw()
|
||||
|
|
@ -1189,7 +1465,7 @@ function editorDraw()
|
|||
end
|
||||
-- The room view draws its status inside its own strip (see the room branch);
|
||||
-- the other views keep the simple centered line.
|
||||
if editorStatus and editorView ~= "room" then
|
||||
if editorStatus and editorView ~= "room" and editorView ~= "wire" then
|
||||
love.graphics.setColor(gColor.white:set())
|
||||
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue