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
|
||||
|
|
|
|||
|
|
@ -59,5 +59,23 @@ function getAllAssets()
|
|||
end
|
||||
end
|
||||
|
||||
-- Wired devices intentionally reuse the familiar switch/door artwork. Their
|
||||
-- white terminal marker is drawn by the entity class, while these synthetic
|
||||
-- asset records make them available to the ordinary palette and brush.
|
||||
local function synthetic(name, source, behavior)
|
||||
local original = globalAssetProperties[source]
|
||||
if not original then return end
|
||||
globalAssetProperties[name] = {
|
||||
directory = original.directory,
|
||||
sprites = original.sprites,
|
||||
cells = original.cells,
|
||||
class = behavior,
|
||||
preload = false,
|
||||
synthetic = true,
|
||||
}
|
||||
end
|
||||
synthetic("wired_switch", "switch", "wired_switch")
|
||||
synthetic("wired_door", "door", "wired_door")
|
||||
|
||||
return BoxVariants.addTo(globalAssetProperties)
|
||||
end
|
||||
|
|
|
|||
12
main.lua
12
main.lua
|
|
@ -53,6 +53,8 @@ function love.load()
|
|||
if gameWorld.levels[initialRoom] then
|
||||
gameWorld.levels[initialRoom] = currentRoom
|
||||
gameWorld:rebuildCollision()
|
||||
gameWorld:resolveCircuits()
|
||||
gameWorld:rebuildCollision()
|
||||
end
|
||||
gameWorld:setLastRoom(currentRoom.name)
|
||||
setEditorRoomName(currentRoom.name)
|
||||
|
|
@ -76,7 +78,7 @@ function love.quit()
|
|||
end
|
||||
|
||||
function love.update(dt)
|
||||
if editorView == "world" then
|
||||
if editorView == "world" or editorView == "wire" then
|
||||
Input:stopMoveRepeat()
|
||||
else
|
||||
local repeatedMove = Input:handler(dt)
|
||||
|
|
@ -94,6 +96,8 @@ function love.draw()
|
|||
love.graphics.setBlendMode("screen")
|
||||
if editorView == "world" then
|
||||
gameWorld:draw()
|
||||
elseif editorView == "wire" then
|
||||
gameWorld:drawWireEditor(wireEditorCamera())
|
||||
else
|
||||
currentRoom:draw()
|
||||
end
|
||||
|
|
@ -173,10 +177,12 @@ function love.keypressed(key)
|
|||
return
|
||||
end
|
||||
|
||||
if editorView ~= "world" and Input.isPlayerMove(key) then
|
||||
if editorView == "wire" and editorWireKeypressed(key) then return end
|
||||
|
||||
if editorView ~= "world" and editorView ~= "wire" and Input.isPlayerMove(key) then
|
||||
Input:beginMove(key)
|
||||
gameWorld:movePlayer(currentRoom, key)
|
||||
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
|
||||
elseif editorView ~= "world" and editorView ~= "wire" and Input.isPlayerSwitch(key) then
|
||||
local c = Input.playerColor(key)
|
||||
gameWorld:switchPlayerColor(currentRoom, c)
|
||||
end
|
||||
|
|
|
|||
206
room.lua
206
room.lua
|
|
@ -8,6 +8,8 @@ require "art"
|
|||
require "assorted"
|
||||
require "switch"
|
||||
require "door"
|
||||
require "wired_switch"
|
||||
require "wired_door"
|
||||
require "dialogue_manager"
|
||||
require "generic_moveable"
|
||||
require "generic_sticky"
|
||||
|
|
@ -17,11 +19,15 @@ require "generic_unmoveable"
|
|||
require "generic_player_blocker"
|
||||
|
||||
local Movement = require "movement"
|
||||
local Wire = require "wire"
|
||||
|
||||
function Room:initialize(t)
|
||||
self.player = {}
|
||||
self.switches = { red = {}, green = {}, blue = {} }
|
||||
self.doors = {}
|
||||
self.wiredSwitches = {}
|
||||
self.wiredDoors = {}
|
||||
self.wires = {}
|
||||
self.collideables = { red = {}, green = {}, blue = {} }
|
||||
self.playerBlockers = { red = {}, green = {}, blue = {} }
|
||||
self.npcs = {}
|
||||
|
|
@ -125,6 +131,7 @@ function Room:update(dt)
|
|||
end
|
||||
|
||||
function Room:draw()
|
||||
self:drawWiresAt(0, 0, width / gridWidth, nil, false)
|
||||
|
||||
-- draw only the channels the player has. Each object just exposes sprites per
|
||||
-- channel via Entity:draw(channel); a door registered on all three channels
|
||||
|
|
@ -135,10 +142,117 @@ function Room:draw()
|
|||
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then gameWorld:drawOverlaps(self, channel) end
|
||||
for _, blocker in pairs(self.playerBlockers[channel]) do blocker:draw(channel) end
|
||||
for _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
|
||||
for _, switch in ipairs(self.wiredSwitches) do switch:draw(channel) end
|
||||
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
|
||||
end
|
||||
end
|
||||
|
||||
function Room:wireTracksAt(cell, create)
|
||||
local row = self.wires[cell.y]
|
||||
if not row and create then row = {}; self.wires[cell.y] = row end
|
||||
local tracks = row and row[cell.x]
|
||||
if not tracks and create then tracks = {}; row[cell.x] = tracks end
|
||||
return tracks
|
||||
end
|
||||
|
||||
function Room:setWireMask(cell, circuitId, mask)
|
||||
if not self:isInBounds(cell) then return false end
|
||||
local tracks = self:wireTracksAt(cell, true)
|
||||
tracks[circuitId] = mask or 0
|
||||
return true
|
||||
end
|
||||
|
||||
function Room:wireCircuitAt(cell)
|
||||
local tracks = self:wireTracksAt(cell)
|
||||
local ids = {}
|
||||
for circuitId in pairs(tracks or {}) do table.insert(ids, circuitId) end
|
||||
table.sort(ids)
|
||||
return ids[1]
|
||||
end
|
||||
|
||||
function Room:removeWire(cell, circuitId)
|
||||
local tracks = self:wireTracksAt(cell)
|
||||
if not tracks or tracks[circuitId] == nil then return false end
|
||||
tracks[circuitId] = nil
|
||||
if not next(tracks) then self.wires[cell.y][cell.x] = nil end
|
||||
if not next(self.wires[cell.y]) then self.wires[cell.y] = nil end
|
||||
return true
|
||||
end
|
||||
|
||||
function Room:drawWiresAt(originX, originY, cellSize, selectedCircuit, editorColors)
|
||||
local ids = {}
|
||||
for _, row in pairs(self.wires) do
|
||||
for _, tracks in pairs(row) do
|
||||
for circuitId in pairs(tracks) do ids[circuitId] = true end
|
||||
end
|
||||
end
|
||||
local ordered = {}
|
||||
for circuitId in pairs(ids) do table.insert(ordered, circuitId) end
|
||||
table.sort(ordered)
|
||||
|
||||
love.graphics.setBlendMode("alpha")
|
||||
local visibleColors = {red = false, green = false, blue = false}
|
||||
for _, color in ipairs(self.colorsPlayerHas or {}) do visibleColors[color] = true end
|
||||
local editorIsOn = editorView ~= nil and editorView ~= "off"
|
||||
for _, circuitId in ipairs(ordered) do
|
||||
local selected = not selectedCircuit or selectedCircuit == circuitId
|
||||
for y, row in pairs(self.wires) do
|
||||
for x, tracks in pairs(row) do
|
||||
local mask = tracks[circuitId]
|
||||
if mask ~= nil then
|
||||
local localCell = {x = x, y = y}
|
||||
local switchColors = gameWorld and gameWorld.wireSwitchColorsAt and
|
||||
gameWorld:wireSwitchColorsAt(self, localCell, circuitId)
|
||||
local drawColors = {}
|
||||
if editorColors then
|
||||
-- Connected circuits retain their ID colour in the wiring tool;
|
||||
-- switchless fragments are neutral so orphans are obvious.
|
||||
table.insert(drawColors, switchColors and Wire.color(circuitId) or {0.52, 0.52, 0.58})
|
||||
elseif switchColors then
|
||||
-- Draw literal RGB passes taken from the switch entities at this
|
||||
-- run's sole terminal. The room's player channels decide which
|
||||
-- passes exist, exactly as they do for every other entity.
|
||||
for _, channel in ipairs({"red", "green", "blue"}) do
|
||||
if switchColors[channel] and visibleColors[channel] then
|
||||
local channelColor = gColor[channel]
|
||||
table.insert(drawColors, {channelColor.r, channelColor.g, channelColor.b})
|
||||
end
|
||||
end
|
||||
elseif editorIsOn then
|
||||
table.insert(drawColors, {0.52, 0.52, 0.58})
|
||||
end
|
||||
|
||||
if #drawColors > 0 then
|
||||
local cx = originX + (x - 0.5) * cellSize
|
||||
local cy = originY + (y - 0.5) * cellSize
|
||||
local lines = {}
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if Wire.has(mask, direction.bit) then
|
||||
table.insert(lines, {cx, cy, cx + direction.x * cellSize / 2, cy + direction.y * cellSize / 2})
|
||||
end
|
||||
end
|
||||
if #lines == 0 then table.insert(lines, {cx, cy, cx, cy}) end
|
||||
-- The dark under-stroke makes later tracks cut a readable gap in
|
||||
-- earlier tracks when two independent circuits cross.
|
||||
love.graphics.setColor(0.03, 0.03, 0.05, selected and 1 or 0.7)
|
||||
love.graphics.setLineWidth(math.max(1.5, cellSize * 0.10))
|
||||
for _, line in ipairs(lines) do love.graphics.line(line) end
|
||||
local wireBlend = (editorColors or not switchColors) and "alpha" or "screen"
|
||||
love.graphics.setBlendMode(wireBlend)
|
||||
love.graphics.setLineWidth(math.max(1, cellSize * 0.045))
|
||||
for _, color in ipairs(drawColors) do
|
||||
love.graphics.setColor(color[1], color[2], color[3], selected and 1 or 0.32)
|
||||
for _, line in ipairs(lines) do love.graphics.line(line) end
|
||||
love.graphics.circle("fill", cx, cy, math.max(0.75, cellSize * 0.0275))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
end
|
||||
|
||||
function Room:createCollideablesMatrix()
|
||||
for color, entities in pairs(self.collideables) do
|
||||
self.collidableMatrices[color] = self:createEntityMatrix(entities)
|
||||
|
|
@ -538,7 +652,8 @@ end
|
|||
-- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/
|
||||
-- "moveable"/"sticky"/"skewer"/"spiky"/"player_blocker"); anything without a .meta defaults to immoveable. npc is the one
|
||||
-- keyword handled directly since it has no art.
|
||||
function Room:createEntity(class, color, x, y)
|
||||
function Room:createEntity(class, color, x, y, data)
|
||||
data = data or {}
|
||||
if class == "npc" then
|
||||
local entity = Sign:new(x, y)
|
||||
self:registerNpc(entity)
|
||||
|
|
@ -554,7 +669,31 @@ function Room:createEntity(class, color, x, y)
|
|||
local behavior = props and props.class or "immoveable"
|
||||
local entity
|
||||
|
||||
if behavior == "door" then
|
||||
if behavior == "wired_door" then
|
||||
if not color then return end
|
||||
local circuitId = data.circuit
|
||||
-- The wire terminal owns identity. A replacement door adopts the route
|
||||
-- already present at its cell; additional RGB layers of a white door adopt
|
||||
-- the first door layer's circuit as well.
|
||||
if not circuitId then
|
||||
circuitId = self:wireCircuitAt({x = x, y = y})
|
||||
end
|
||||
if not circuitId then
|
||||
for _, existingDoor in ipairs(self.wiredDoors) do
|
||||
if existingDoor:occupiesCell({x = x, y = y}) then
|
||||
circuitId = existingDoor.circuitId
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not circuitId and gameWorld then circuitId = gameWorld:newCircuitId() end
|
||||
circuitId = circuitId or ("wire_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000000)))
|
||||
entity = WiredDoor:new(x, y, color, circuitId)
|
||||
self:registerWiredDoor(entity, color)
|
||||
elseif behavior == "wired_switch" then
|
||||
entity = WiredSwitch:new(x, y, color)
|
||||
self:registerWiredSwitch(entity)
|
||||
elseif behavior == "door" then
|
||||
-- Old migrated rooms stored a colourless shared door. Expand that legacy
|
||||
-- record into ordinary colour-layer door objects on load.
|
||||
if not color then
|
||||
|
|
@ -622,6 +761,15 @@ function Room:registerSwitch(entityPointer, color)
|
|||
table.insert(self.switches[color], entityPointer)
|
||||
end
|
||||
|
||||
function Room:registerWiredDoor(entityPointer, color)
|
||||
table.insert(self.wiredDoors, entityPointer)
|
||||
self:registerCollidable(entityPointer, color)
|
||||
end
|
||||
|
||||
function Room:registerWiredSwitch(entityPointer)
|
||||
table.insert(self.wiredSwitches, entityPointer)
|
||||
end
|
||||
|
||||
function Room:registerPlayer(entityPointer, color)
|
||||
table.insert(self.colorsPlayerHas, color)
|
||||
self.player[color] = entityPointer
|
||||
|
|
@ -691,6 +839,15 @@ function Room:attemptDelete(mouseX, mouseY, color)
|
|||
for index = #self.doors, 1, -1 do
|
||||
if removedEntities[self.doors[index]] then table.remove(self.doors, index) end
|
||||
end
|
||||
for index = #self.wiredDoors, 1, -1 do
|
||||
if removedEntities[self.wiredDoors[index]] then table.remove(self.wiredDoors, index) end
|
||||
end
|
||||
for index = #self.wiredSwitches, 1, -1 do
|
||||
if self.wiredSwitches[index]:occupiesCell(cell) and
|
||||
(not color or color == "white" or self.wiredSwitches[index].color:getname() == color) then
|
||||
table.remove(self.wiredSwitches, index)
|
||||
end
|
||||
end
|
||||
-- NPCs are stamped into every collision layer, but are kept in their own
|
||||
-- registry rather than in self.collideables. Remove them directly so a
|
||||
-- right-click works no matter which editor colour is currently selected.
|
||||
|
|
@ -746,7 +903,7 @@ end
|
|||
|
||||
function Room:deserialize(saveTable)
|
||||
for _, obj in ipairs(saveTable.objects or {}) do
|
||||
self:createEntity(obj.class, obj.color, obj.x, obj.y)
|
||||
self:createEntity(obj.class, obj.color, obj.x, obj.y, obj)
|
||||
-- createEntity turns every loaded player on; honour a saved active flag so
|
||||
-- copies that were off at save time stay off (older savs lack the field and
|
||||
-- keep the old all-on behaviour).
|
||||
|
|
@ -754,6 +911,11 @@ function Room:deserialize(saveTable)
|
|||
self:setActiveColor(obj.color, obj.active)
|
||||
end
|
||||
end
|
||||
for _, wire in ipairs(saveTable.wires or {}) do
|
||||
if wire.x and wire.y and wire.circuit then
|
||||
self:setWireMask({x = wire.x, y = wire.y}, wire.circuit, wire.edges or 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Room:save()
|
||||
|
|
@ -774,12 +936,43 @@ function Room:saveToSource(name, savePlayerPositions)
|
|||
return writeToSource("rooms/" .. name .. ".sav", room)
|
||||
end
|
||||
|
||||
function Room:serializeWires()
|
||||
local wires = {}
|
||||
for y, row in pairs(self.wires) do
|
||||
for x, tracks in pairs(row) do
|
||||
for circuitId, edges in pairs(tracks) do
|
||||
table.insert(wires, {x = x, y = y, circuit = circuitId, edges = edges})
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(wires, function(a, b)
|
||||
if a.y ~= b.y then return a.y < b.y end
|
||||
if a.x ~= b.x then return a.x < b.x end
|
||||
return a.circuit < b.circuit
|
||||
end)
|
||||
return wires
|
||||
end
|
||||
|
||||
-- Cross-room wire editing may touch a neighbour loaded from runtime state.
|
||||
-- Merge only its authored wire slice so saving a route cannot accidentally
|
||||
-- bake that neighbour's runtime block/player positions into the source level.
|
||||
function Room:saveWiresToSource()
|
||||
if not self.name then return false end
|
||||
local raw = readFromSource("rooms/" .. self.name .. ".sav")
|
||||
local ok, saveTable = raw and pcall(TSerial.unpack, raw)
|
||||
if not ok or type(saveTable) ~= "table" then saveTable = self:serialize(false) end
|
||||
saveTable.wires = self:serializeWires()
|
||||
return writeToSource("rooms/" .. self.name .. ".sav", TSerial.pack(saveTable, nil, true))
|
||||
end
|
||||
|
||||
function Room:serialize(includePlayers)
|
||||
local objects = {}
|
||||
|
||||
local function emit(class, color, entity)
|
||||
local pos = entity:getGridPos()
|
||||
table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y })
|
||||
local object = { class = class, color = color, x = pos.x, y = pos.y }
|
||||
if entity.circuitId then object.circuit = entity.circuitId end
|
||||
table.insert(objects, object)
|
||||
end
|
||||
|
||||
if includePlayers ~= false then
|
||||
|
|
@ -812,12 +1005,15 @@ function Room:serialize(includePlayers)
|
|||
emit("switch", color, switch)
|
||||
end
|
||||
end
|
||||
for _, switch in ipairs(self.wiredSwitches) do
|
||||
emit("wired_switch", switch.color:getname(), switch)
|
||||
end
|
||||
|
||||
for _, npc in ipairs(self.npcs) do
|
||||
table.insert(objects, { class = npc.saveClass or "npc", x = npc.x, y = npc.y })
|
||||
end
|
||||
|
||||
return { objects = objects }
|
||||
return { objects = objects, wires = self:serializeWires() }
|
||||
end
|
||||
|
||||
function nextRoom(pos)
|
||||
|
|
|
|||
207
rooms/room_08_13.sav
Normal file
207
rooms/room_08_13.sav
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
{
|
||||
objects={
|
||||
{
|
||||
color="green",
|
||||
x=3,
|
||||
y=5,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=3,
|
||||
y=5,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=3,
|
||||
y=5,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=2,
|
||||
y=4,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=2,
|
||||
y=9,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=3,
|
||||
y=6,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=2,
|
||||
y=9,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=3,
|
||||
y=6,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=3,
|
||||
y=9,
|
||||
class="wired_switch"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=3,
|
||||
y=9,
|
||||
class="wired_switch"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=3,
|
||||
y=9,
|
||||
class="wired_switch"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=3,
|
||||
y=4,
|
||||
class="wired_switch"
|
||||
}
|
||||
},
|
||||
wires={
|
||||
{
|
||||
edges=6,
|
||||
x=4,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=5,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=6,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=12,
|
||||
x=7,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=6,
|
||||
x=10,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=11,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=2,
|
||||
x=3,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=9,
|
||||
x=4,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=3,
|
||||
x=7,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=8,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=9,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=9,
|
||||
x=10,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=2,
|
||||
x=3,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=4,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=5,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=6,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=7,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=8,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=9,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=10,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=11,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
}
|
||||
}
|
||||
}
|
||||
217
rooms/room_09_13.sav
Normal file
217
rooms/room_09_13.sav
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
{
|
||||
objects={
|
||||
{
|
||||
color="green",
|
||||
x=3,
|
||||
y=7,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=3,
|
||||
y=7,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=3,
|
||||
y=7,
|
||||
active=true,
|
||||
class="player"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=9,
|
||||
y=8,
|
||||
circuit="wire_6a7bdb2a_0001",
|
||||
class="wired_door"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=5,
|
||||
y=8,
|
||||
class="box"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=9,
|
||||
y=8,
|
||||
circuit="wire_6a7bdb2a_0001",
|
||||
class="wired_door"
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
x=7,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a",
|
||||
class="wired_door"
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
x=9,
|
||||
y=8,
|
||||
circuit="wire_6a7bdb2a_0001",
|
||||
class="wired_door"
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
x=5,
|
||||
y=7,
|
||||
class="wired_switch"
|
||||
}
|
||||
},
|
||||
wires={
|
||||
{
|
||||
edges=12,
|
||||
x=1,
|
||||
y=3,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=3,
|
||||
x=1,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=2,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=3,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=4,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=5,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=6,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=8,
|
||||
x=7,
|
||||
y=4,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=4,
|
||||
x=7,
|
||||
y=4,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=5,
|
||||
x=7,
|
||||
y=5,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=6,
|
||||
x=5,
|
||||
y=6,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=6,
|
||||
y=6,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=9,
|
||||
x=7,
|
||||
y=6,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=0,
|
||||
x=5,
|
||||
y=7,
|
||||
circuit="wire_6a7bd969_000a"
|
||||
},
|
||||
{
|
||||
edges=1,
|
||||
x=5,
|
||||
y=7,
|
||||
circuit="wire_6a7bdf16_0001"
|
||||
},
|
||||
{
|
||||
edges=4,
|
||||
x=9,
|
||||
y=8,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=1,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=2,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=3,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=4,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=5,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=6,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=7,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=10,
|
||||
x=8,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
},
|
||||
{
|
||||
edges=9,
|
||||
x=9,
|
||||
y=9,
|
||||
circuit="wire_6a7bdb2a_0001"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
rooms/room_13_08.sav
Normal file
4
rooms/room_13_08.sav
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
objects={
|
||||
}
|
||||
}
|
||||
4
rooms/room_14_07.sav
Normal file
4
rooms/room_14_07.sav
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
objects={
|
||||
}
|
||||
}
|
||||
|
|
@ -1,255 +1,369 @@
|
|||
{
|
||||
objects={
|
||||
{
|
||||
color="blue",
|
||||
class="door",
|
||||
color="blue",
|
||||
x=6,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=2,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=4,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=1,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="blue",
|
||||
x=11,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="blue",
|
||||
x=1,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box",
|
||||
color="blue",
|
||||
x=3,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_1x2_1-1",
|
||||
color="blue",
|
||||
x=4,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_1x2_1-1",
|
||||
color="blue",
|
||||
x=5,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_2x2_11-10",
|
||||
color="blue",
|
||||
x=6,
|
||||
y=5
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="blue",
|
||||
x=7,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="blue",
|
||||
x=9,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="door",
|
||||
color="red",
|
||||
x=6,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=2,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=4,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=1,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="red",
|
||||
x=11,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="red",
|
||||
x=1,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box",
|
||||
color="red",
|
||||
x=3,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_1x2_1-1",
|
||||
color="red",
|
||||
x=4,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_1x2_1-1",
|
||||
color="red",
|
||||
x=5,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_2x2_11-10",
|
||||
color="red",
|
||||
x=6,
|
||||
y=5
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="red",
|
||||
x=7,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="red",
|
||||
x=9,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="door",
|
||||
color="green",
|
||||
x=6,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=2,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=2,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=2,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=4,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=1,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="static_wall",
|
||||
color="green",
|
||||
x=11,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="green",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=4,
|
||||
y=1
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=4,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=4,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=5,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=8,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=10,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=11
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box",
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=1,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=11
|
||||
x=5,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_1x2_1-1",
|
||||
color="green",
|
||||
class="static_wall",
|
||||
x=11,
|
||||
y=11
|
||||
},
|
||||
{
|
||||
color="red",
|
||||
class="switch",
|
||||
x=6,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
color="blue",
|
||||
class="switch",
|
||||
x=6,
|
||||
class="spiky_box_1x2_1-1",
|
||||
color="green",
|
||||
x=7,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="box_2x2_11-10",
|
||||
color="green",
|
||||
class="switch",
|
||||
x=6,
|
||||
x=8,
|
||||
y=5
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="green",
|
||||
x=9,
|
||||
y=6
|
||||
},
|
||||
{
|
||||
class="spiky_box_2x1_11",
|
||||
color="green",
|
||||
x=11,
|
||||
y=6
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1107
rooms/village_3.sav
1107
rooms/village_3.sav
File diff suppressed because it is too large
Load diff
116
rooms/world.sav
116
rooms/world.sav
|
|
@ -1,212 +1,232 @@
|
|||
{
|
||||
height=18,
|
||||
lastRoom="white_primary",
|
||||
rooms={
|
||||
{
|
||||
name="3",
|
||||
x=10,
|
||||
name="3",
|
||||
y=10
|
||||
},
|
||||
{
|
||||
x=12,
|
||||
name="5",
|
||||
x=12,
|
||||
y=10
|
||||
},
|
||||
{
|
||||
name="village_1",
|
||||
x=12,
|
||||
name="village_1",
|
||||
y=9
|
||||
},
|
||||
{
|
||||
name="village_3",
|
||||
x=13,
|
||||
y=8
|
||||
},
|
||||
{
|
||||
x=11,
|
||||
name="village_4",
|
||||
x=11,
|
||||
y=8
|
||||
},
|
||||
{
|
||||
name="yellow_1",
|
||||
x=11,
|
||||
name="yellow_1",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
name="yellow_3",
|
||||
x=11,
|
||||
name="yellow_3",
|
||||
y=6
|
||||
},
|
||||
{
|
||||
name="yellow_2",
|
||||
x=12,
|
||||
name="yellow_2",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
name="yellow_4",
|
||||
x=11,
|
||||
name="yellow_4",
|
||||
y=5
|
||||
},
|
||||
{
|
||||
name="yellow_5",
|
||||
x=11,
|
||||
name="yellow_5",
|
||||
y=4
|
||||
},
|
||||
{
|
||||
name="yellow_6",
|
||||
x=10,
|
||||
name="yellow_6",
|
||||
y=5
|
||||
},
|
||||
{
|
||||
x=1,
|
||||
name="default",
|
||||
x=1,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
x=3,
|
||||
name="old_yellow_3",
|
||||
x=3,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="test_patt",
|
||||
x=5,
|
||||
name="test_patt",
|
||||
y=1
|
||||
},
|
||||
{
|
||||
x=7,
|
||||
name="test_patt_update",
|
||||
x=7,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
x=9,
|
||||
name="white_asymmetrical",
|
||||
x=9,
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="white_ok",
|
||||
x=11,
|
||||
name="white_ok",
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="white_primary",
|
||||
x=13,
|
||||
name="white_primary",
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="yellow_arches",
|
||||
x=15,
|
||||
name="yellow_arches",
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="yellow_asymmetrical",
|
||||
x=17,
|
||||
name="yellow_asymmetrical",
|
||||
y=1
|
||||
},
|
||||
{
|
||||
name="yellow_cc_moustache",
|
||||
x=1,
|
||||
name="yellow_cc_moustache",
|
||||
y=3
|
||||
},
|
||||
{
|
||||
name="yellow_color_changing_levvel_1",
|
||||
x=3,
|
||||
name="yellow_color_changing_levvel_1",
|
||||
y=3
|
||||
},
|
||||
{
|
||||
name="yellow_smile",
|
||||
x=7,
|
||||
name="yellow_smile",
|
||||
y=3
|
||||
},
|
||||
{
|
||||
name="2",
|
||||
x=9,
|
||||
name="2",
|
||||
y=10
|
||||
},
|
||||
{
|
||||
name="room_11_09",
|
||||
x=11,
|
||||
name="room_11_09",
|
||||
y=9
|
||||
},
|
||||
{
|
||||
name="yellow_skull",
|
||||
x=9,
|
||||
name="yellow_skull",
|
||||
y=3
|
||||
},
|
||||
{
|
||||
name="jorge_room",
|
||||
x=5,
|
||||
name="jorge_room",
|
||||
y=5
|
||||
},
|
||||
{
|
||||
name="4",
|
||||
x=11,
|
||||
name="4",
|
||||
y=10
|
||||
},
|
||||
{
|
||||
name="room_01_05",
|
||||
x=1,
|
||||
name="room_01_05",
|
||||
y=5
|
||||
},
|
||||
{
|
||||
name="ok",
|
||||
x=2,
|
||||
name="ok",
|
||||
y=8
|
||||
},
|
||||
{
|
||||
name="room_05_18",
|
||||
x=5,
|
||||
name="room_05_18",
|
||||
y=18
|
||||
},
|
||||
{
|
||||
name="village_start",
|
||||
x=9,
|
||||
name="village_start",
|
||||
y=11
|
||||
},
|
||||
{
|
||||
name="spiky_test",
|
||||
x=4,
|
||||
name="spiky_test",
|
||||
y=8
|
||||
},
|
||||
{
|
||||
x=5,
|
||||
name="corner_trimino_test",
|
||||
x=5,
|
||||
y=7
|
||||
},
|
||||
{
|
||||
name="tetromino_loop_t",
|
||||
x=6,
|
||||
name="tetromino_loop_t",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
name="corner_trimino_test_copy",
|
||||
x=5,
|
||||
name="corner_trimino_test_copy",
|
||||
y=6
|
||||
},
|
||||
{
|
||||
name="blocker_test",
|
||||
x=7,
|
||||
name="blocker_test",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
name="spiky_test_2",
|
||||
x=5,
|
||||
name="spiky_test_2",
|
||||
y=8
|
||||
},
|
||||
{
|
||||
name="spiky_test_3",
|
||||
x=5,
|
||||
name="spiky_test_3",
|
||||
y=9
|
||||
},
|
||||
{
|
||||
name="jorge2",
|
||||
x=3,
|
||||
name="jorge2",
|
||||
y=14
|
||||
},
|
||||
{
|
||||
name="room_08_07",
|
||||
x=8,
|
||||
name="room_08_07",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
x=13,
|
||||
name="village_3",
|
||||
y=8
|
||||
},
|
||||
{
|
||||
x=12,
|
||||
name="village_2",
|
||||
y=8
|
||||
},
|
||||
{
|
||||
x=14,
|
||||
name="room_14_07",
|
||||
y=7
|
||||
},
|
||||
{
|
||||
x=9,
|
||||
name="room_09_13",
|
||||
y=13
|
||||
},
|
||||
{
|
||||
x=8,
|
||||
name="room_08_13",
|
||||
y=13
|
||||
}
|
||||
},
|
||||
width=18
|
||||
lastRoom="room_09_13",
|
||||
width=18,
|
||||
height=18
|
||||
}
|
||||
206
tests/test_wire.lua
Normal file
206
tests/test_wire.lua
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package.path = "./?.lua;" .. package.path
|
||||
|
||||
class = require "libs/middleclass"
|
||||
local Wire = require "wire"
|
||||
require "world"
|
||||
|
||||
gridWidth, gridHeight = 2, 2
|
||||
|
||||
local passed = 0
|
||||
local function test(name, fn)
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then
|
||||
io.stderr:write("FAIL: " .. name .. "\n" .. err .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
passed = passed + 1
|
||||
end
|
||||
|
||||
local function assertTrue(value, message)
|
||||
if not value then error(message or "expected true", 2) end
|
||||
end
|
||||
|
||||
local function assertFalse(value, message)
|
||||
if value then error(message or "expected false", 2) end
|
||||
end
|
||||
|
||||
local function switch(color, x, y)
|
||||
local entity = {x = x, y = y, activated = false}
|
||||
entity.color = {getname = function() return color end}
|
||||
function entity:getGridPos() return {x = self.x, y = self.y} end
|
||||
function entity:activate() self.activated = true end
|
||||
function entity:deactivate() self.activated = false end
|
||||
return entity
|
||||
end
|
||||
|
||||
local function door(circuitId, x, y)
|
||||
local entity = {circuitId = circuitId, x = x, y = y, powered = false}
|
||||
function entity:getGridPos() return {x = self.x, y = self.y} end
|
||||
function entity:setPowered(powered) self.powered = powered end
|
||||
return entity
|
||||
end
|
||||
|
||||
local function makeWorld(secondSwitch)
|
||||
local blueSwitch = switch("blue", 2, 1)
|
||||
local wiredDoor = door("circuit_a", 1, 1)
|
||||
local roomA = {
|
||||
name = "a", player = {}, wiredSwitches = {}, wiredDoors = {wiredDoor},
|
||||
wires = {[1] = {
|
||||
[1] = {circuit_a = Wire.EAST},
|
||||
[2] = {circuit_a = Wire.WEST + Wire.EAST},
|
||||
}},
|
||||
}
|
||||
local roomB = {
|
||||
name = "b", player = {}, wiredSwitches = {blueSwitch}, wiredDoors = {},
|
||||
wires = {[1] = {
|
||||
[1] = {circuit_a = Wire.WEST + Wire.EAST},
|
||||
[2] = {circuit_a = Wire.WEST},
|
||||
}},
|
||||
}
|
||||
local function wireCircuitAt(room, cell)
|
||||
local tracks = room.wires[cell.y] and room.wires[cell.y][cell.x]
|
||||
for circuitId in pairs(tracks or {}) do return circuitId end
|
||||
end
|
||||
roomA.wireCircuitAt = wireCircuitAt
|
||||
roomB.wireCircuitAt = wireCircuitAt
|
||||
if secondSwitch then table.insert(roomB.wiredSwitches, secondSwitch) end
|
||||
|
||||
local instance = setmetatable({
|
||||
levels = {a = roomA, b = roomB},
|
||||
roomByName = {a = {name = "a", x = 1, y = 1}, b = {name = "b", x = 2, y = 1}},
|
||||
collidableMatrices = {red = {}, green = {}, blue = {}},
|
||||
}, {__index = World})
|
||||
function instance:roomCell(cell)
|
||||
if cell.x <= gridWidth then return roomA, {x = cell.x, y = cell.y} end
|
||||
return roomB, {x = cell.x - gridWidth, y = cell.y}
|
||||
end
|
||||
local function wireTracksAt(room, cell, create)
|
||||
local row = room.wires[cell.y]
|
||||
if not row and create then row = {}; room.wires[cell.y] = row end
|
||||
local tracks = row and row[cell.x]
|
||||
if not tracks and create then tracks = {}; row[cell.x] = tracks end
|
||||
return tracks
|
||||
end
|
||||
roomA.wireTracksAt = wireTracksAt
|
||||
roomB.wireTracksAt = wireTracksAt
|
||||
return instance, roomA, roomB, wiredDoor, blueSwitch
|
||||
end
|
||||
|
||||
test("direction masks add and remove without duplicating bits", function()
|
||||
local mask = Wire.add(0, Wire.EAST)
|
||||
mask = Wire.add(mask, Wire.EAST)
|
||||
assertTrue(mask == Wire.EAST)
|
||||
assertTrue(Wire.has(mask, Wire.EAST))
|
||||
assertFalse(Wire.has(mask, Wire.WEST))
|
||||
assertTrue(Wire.remove(mask, Wire.EAST) == 0)
|
||||
end)
|
||||
|
||||
test("orphaned and overlapping wire cells remain selectable without a door", function()
|
||||
local world, roomA = makeWorld()
|
||||
roomA.wires[1][1].orphan_b = Wire.SOUTH
|
||||
local top = world:circuitAt({x = 1, y = 1})
|
||||
assertTrue(top == "orphan_b", "top orphaned circuit was not selectable")
|
||||
local preferred = world:circuitAt({x = 1, y = 1}, "circuit_a")
|
||||
assertTrue(preferred == "circuit_a", "selected overlapping circuit was not preferred")
|
||||
end)
|
||||
|
||||
test("a white switch powers every layer of a white door", function()
|
||||
local world, roomA, roomB = makeWorld()
|
||||
roomA.wiredDoors = {
|
||||
door("circuit_a", 1, 1),
|
||||
door("old_green_id", 1, 1),
|
||||
door("old_blue_id", 1, 1),
|
||||
}
|
||||
roomB.wiredSwitches = {
|
||||
switch("red", 2, 1),
|
||||
switch("green", 2, 1),
|
||||
switch("blue", 2, 1),
|
||||
}
|
||||
world.collidableMatrices.red[1] = {[4] = {}}
|
||||
world.collidableMatrices.green[1] = {[4] = {}}
|
||||
world.collidableMatrices.blue[1] = {[4] = {}}
|
||||
world:resolveCircuits()
|
||||
|
||||
for _, wiredDoor in ipairs(roomA.wiredDoors) do
|
||||
assertTrue(wiredDoor.circuitId == "circuit_a", "door layer did not adopt terminal circuit")
|
||||
assertTrue(world.circuitState.doorPowered[wiredDoor], "white door layer remained locked")
|
||||
end
|
||||
local color = world:wireColorAt(roomA, {x = 1, y = 1}, "circuit_a")
|
||||
assertTrue(color[1] == 1 and color[2] == 1 and color[3] == 1, "white switch did not produce white wire")
|
||||
local redGreenOnly = world:wireColorAt(roomA, {x = 1, y = 1}, "circuit_a", {red = true, green = true})
|
||||
assertTrue(redGreenOnly[1] == 1 and redGreenOnly[2] == 1 and redGreenOnly[3] == 0,
|
||||
"white wire did not obey the room's visible player channels")
|
||||
end)
|
||||
|
||||
test("world matrices join a circuit across a room boundary", function()
|
||||
local world, _, _, wiredDoor, blueSwitch = makeWorld()
|
||||
world.collidableMatrices.blue[1] = {[4] = {}}
|
||||
world:resolveCircuits()
|
||||
assertTrue(world.wireMatrix[1][2].circuit_a ~= nil)
|
||||
assertTrue(world.wireMatrix[1][3].circuit_a ~= nil)
|
||||
assertTrue(world.wiredDoorMatrix[1][1][1].entity == wiredDoor)
|
||||
assertTrue(world.wiredSwitchMatrix[1][4][1].entity == blueSwitch)
|
||||
assertTrue(world.circuitState.switchPressed[blueSwitch])
|
||||
assertTrue(world.circuitState.doorPowered[wiredDoor])
|
||||
assertTrue(wiredDoor.powered)
|
||||
assertTrue(world:wireColorAt(world.levels.a, {x = 1, y = 1}, "circuit_a", {red = true, green = true}) == nil,
|
||||
"blue wire leaked into a room without a blue player channel")
|
||||
local visibleBlue = world:wireColorAt(world.levels.a, {x = 1, y = 1}, "circuit_a", {blue = true})
|
||||
assertTrue(visibleBlue[1] == 0 and visibleBlue[2] == 0 and visibleBlue[3] == 1,
|
||||
"blue wire did not render in a room with a blue player channel")
|
||||
end)
|
||||
|
||||
test("wire color is re-derived when its terminal switch changes", function()
|
||||
local world, roomA, roomB = makeWorld()
|
||||
world:resolveCircuits()
|
||||
local colors = world:wireSwitchColorsAt(roomA, {x = 1, y = 1}, "circuit_a")
|
||||
assertTrue(colors.blue and not colors.red)
|
||||
roomB.wiredSwitches = {switch("red", 2, 1)}
|
||||
world:resolveCircuits()
|
||||
colors = world:wireSwitchColorsAt(roomA, {x = 1, y = 1}, "circuit_a")
|
||||
assertTrue(colors.red and not colors.blue, "wire retained the deleted terminal switch color")
|
||||
end)
|
||||
|
||||
test("a switchless wire component has no gameplay color", function()
|
||||
local world, roomA, roomB = makeWorld()
|
||||
roomB.wiredSwitches = {}
|
||||
world:resolveCircuits()
|
||||
assertTrue(world:wireSwitchColorsAt(roomA, {x = 1, y = 1}, "circuit_a") == nil)
|
||||
assertTrue(world:wireColorAt(roomA, {x = 1, y = 1}, "circuit_a") == nil)
|
||||
end)
|
||||
|
||||
test("a door requires every independent switch run at its terminal", function()
|
||||
local greenSwitch = switch("green", 1, 2)
|
||||
local world, roomA, _, wiredDoor = makeWorld()
|
||||
table.insert(roomA.wiredSwitches, greenSwitch)
|
||||
roomA.wires[1][1].circuit_b = Wire.SOUTH
|
||||
roomA.wires[2] = {[1] = {circuit_b = Wire.NORTH}}
|
||||
world.collidableMatrices.blue[1] = {[4] = {}}
|
||||
world:resolveCircuits()
|
||||
assertFalse(world.circuitState.doorPowered[wiredDoor])
|
||||
world.collidableMatrices.green[2] = {[1] = {}}
|
||||
world:resolveCircuits()
|
||||
assertTrue(world.circuitState.doorPowered[wiredDoor])
|
||||
end)
|
||||
|
||||
test("one run cannot terminate at two different switches", function()
|
||||
local second = switch("red", 1, 2)
|
||||
local world, _, roomB = makeWorld(second)
|
||||
world:resolveCircuits()
|
||||
local rooms, err = world:addWireConnection({x = 3, y = 1}, {x = 3, y = 2}, "circuit_a")
|
||||
assertTrue(#rooms == 0 and err ~= nil, "second switch terminal was accepted")
|
||||
assertTrue(roomB.wires[2] == nil or roomB.wires[2][1] == nil,
|
||||
"rejected second terminal left a connected wire cell")
|
||||
local colors = world:wireSwitchColorsAt(world.levels.a, {x = 1, y = 1}, "circuit_a")
|
||||
assertTrue(colors.blue and not colors.red, "run stopped deriving color from its sole terminal switch")
|
||||
end)
|
||||
|
||||
test("matching IDs do not bridge a gap or one-sided room edge", function()
|
||||
local world, _, roomB, wiredDoor = makeWorld()
|
||||
world.collidableMatrices.blue[1] = {[4] = {}}
|
||||
roomB.wires[1][1].circuit_a = Wire.EAST -- no reciprocal WEST edge
|
||||
world:resolveCircuits()
|
||||
assertFalse(world.circuitState.doorPowered[wiredDoor])
|
||||
end)
|
||||
|
||||
io.write("PASS: " .. passed .. " wire tests\n")
|
||||
55
wire.lua
Normal file
55
wire.lua
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
local Wire = {
|
||||
NORTH = 1,
|
||||
EAST = 2,
|
||||
SOUTH = 4,
|
||||
WEST = 8,
|
||||
}
|
||||
|
||||
Wire.directions = {
|
||||
{bit = Wire.NORTH, opposite = Wire.SOUTH, x = 0, y = -1},
|
||||
{bit = Wire.EAST, opposite = Wire.WEST, x = 1, y = 0},
|
||||
{bit = Wire.SOUTH, opposite = Wire.NORTH, x = 0, y = 1},
|
||||
{bit = Wire.WEST, opposite = Wire.EAST, x = -1, y = 0},
|
||||
}
|
||||
|
||||
function Wire.has(mask, bit)
|
||||
return math.floor((mask or 0) / bit) % 2 == 1
|
||||
end
|
||||
|
||||
function Wire.add(mask, bit)
|
||||
mask = mask or 0
|
||||
return Wire.has(mask, bit) and mask or mask + bit
|
||||
end
|
||||
|
||||
function Wire.remove(mask, bit)
|
||||
mask = mask or 0
|
||||
return Wire.has(mask, bit) and mask - bit or mask
|
||||
end
|
||||
|
||||
function Wire.directionBetween(a, b)
|
||||
local dx, dy = b.x - a.x, b.y - a.y
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if direction.x == dx and direction.y == dy then return direction end
|
||||
end
|
||||
end
|
||||
|
||||
-- Circuit colours are deliberately independent from the RGB gameplay layers.
|
||||
-- A stable hash makes a route keep the same editor colour after save/load.
|
||||
local palette = {
|
||||
{1.00, 0.78, 0.20},
|
||||
{0.20, 0.92, 0.92},
|
||||
{1.00, 0.42, 0.72},
|
||||
{0.62, 1.00, 0.32},
|
||||
{0.72, 0.55, 1.00},
|
||||
{1.00, 0.55, 0.22},
|
||||
}
|
||||
|
||||
function Wire.color(circuitId)
|
||||
local hash = 0
|
||||
for index = 1, #(circuitId or "") do
|
||||
hash = (hash * 33 + circuitId:byte(index)) % 65521
|
||||
end
|
||||
return palette[hash % #palette + 1]
|
||||
end
|
||||
|
||||
return Wire
|
||||
30
wired_door.lua
Normal file
30
wired_door.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
require "door"
|
||||
|
||||
local Wire = require "wire"
|
||||
|
||||
WiredDoor = class("WiredDoor", Door)
|
||||
|
||||
function WiredDoor:initialize(x, y, color, circuitId)
|
||||
Door.initialize(self, x, y, color)
|
||||
self.circuitId = circuitId
|
||||
self.wired = true
|
||||
end
|
||||
|
||||
function WiredDoor:setPowered(powered)
|
||||
if powered then
|
||||
if self.locked then self:unlock() end
|
||||
elseif not self.locked then
|
||||
self:lock()
|
||||
end
|
||||
self:refreshSprites()
|
||||
end
|
||||
|
||||
function WiredDoor:draw(channel)
|
||||
Entity.draw(self, channel)
|
||||
if not self.sprites[channel] then return end
|
||||
local p = self:getDrawPos()
|
||||
local color = Wire.color(self.circuitId)
|
||||
love.graphics.setColor(color[1], color[2], color[3], 1)
|
||||
love.graphics.setLineWidth(math.max(1, drawScale))
|
||||
love.graphics.circle("line", p.x + 8 * drawScale, p.y + 8 * drawScale, 3 * drawScale)
|
||||
end
|
||||
17
wired_switch.lua
Normal file
17
wired_switch.lua
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
require "switch"
|
||||
|
||||
WiredSwitch = class("WiredSwitch", Switch)
|
||||
|
||||
function WiredSwitch:initialize(x, y, color)
|
||||
Switch.initialize(self, x, y, color)
|
||||
self.wired = true
|
||||
end
|
||||
|
||||
function WiredSwitch:draw(channel)
|
||||
Entity.draw(self, channel)
|
||||
if not self.sprites[channel] then return end
|
||||
local p = self:getDrawPos()
|
||||
love.graphics.setColor(1, 1, 1, 0.9)
|
||||
love.graphics.setLineWidth(math.max(1, drawScale))
|
||||
love.graphics.rectangle("line", p.x + 5 * drawScale, p.y + 5 * drawScale, 6 * drawScale, 6 * drawScale)
|
||||
end
|
||||
409
world.lua
409
world.lua
|
|
@ -1,10 +1,11 @@
|
|||
World = class("World")
|
||||
|
||||
local Movement = require "movement"
|
||||
local Wire = require "wire"
|
||||
|
||||
-- The world is deliberately only a layout: rooms continue to own their puzzle
|
||||
-- state. Keeping the two separate lets the editor move a room without
|
||||
-- rewriting its level file.
|
||||
-- Rooms own authored slices (objects and wire fragments). World composes those
|
||||
-- slices into authoritative live collision and circuit matrices, so moving a
|
||||
-- room changes placement without rewriting its level file.
|
||||
function World:initialize()
|
||||
self.width, self.height = 18, 18
|
||||
self.rooms = {}
|
||||
|
|
@ -14,6 +15,7 @@ function World:initialize()
|
|||
self:load()
|
||||
self:loadRuntimeState()
|
||||
self.levels = {}
|
||||
self.circuitSerial = 0
|
||||
|
||||
end
|
||||
|
||||
|
|
@ -22,6 +24,8 @@ function World:loadLevels()
|
|||
-- through the same runtime-first loader, not just the initially visible one.
|
||||
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name, runtime = true} end
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
|
||||
-- The editor can replace a level with a freshly loaded authored Room. Keep the
|
||||
|
|
@ -30,9 +34,26 @@ function World:setLevel(room)
|
|||
if not room or not room.name or not self:roomLocation(room.name) then return false end
|
||||
self.levels[room.name] = room
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
return true
|
||||
end
|
||||
|
||||
function World:newCircuitId()
|
||||
local used = {}
|
||||
for _, room in pairs(self.levels or {}) do
|
||||
for _, door in ipairs(room.wiredDoors or {}) do used[door.circuitId] = true end
|
||||
for _, row in pairs(room.wires or {}) do
|
||||
for _, tracks in pairs(row) do for circuitId in pairs(tracks) do used[circuitId] = true end end
|
||||
end
|
||||
end
|
||||
repeat
|
||||
self.circuitSerial = self.circuitSerial + 1
|
||||
local id = string.format("wire_%08x_%04x", os.time() % 0xffffffff, self.circuitSerial)
|
||||
if not used[id] then return id end
|
||||
until false
|
||||
end
|
||||
|
||||
-- Runtime state is one logical world snapshot: each level slice is written to
|
||||
-- LÖVE's save directory, and this companion file says which slice was active.
|
||||
-- The authored world layout remains source-only for the editor.
|
||||
|
|
@ -128,7 +149,13 @@ end
|
|||
function World:placeRoom(name, x, y)
|
||||
if not name or not self:isInBounds(x, y) then return false end
|
||||
self:_putRoom(name, x, y)
|
||||
return self:save()
|
||||
local saved = self:save()
|
||||
if self.levels then
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
return saved
|
||||
end
|
||||
|
||||
function World:removeRoom(x, y)
|
||||
|
|
@ -138,7 +165,13 @@ function World:removeRoom(x, y)
|
|||
self.roomByName[room.name] = nil
|
||||
table.remove(self.rooms, i)
|
||||
if self.lastRoom == room.name then self.lastRoom = nil end
|
||||
return self:save(), room.name
|
||||
local saved = self:save()
|
||||
if self.levels then
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
return saved, room.name
|
||||
end
|
||||
end
|
||||
return false
|
||||
|
|
@ -238,6 +271,292 @@ function World:roomCell(cell)
|
|||
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
||||
end
|
||||
|
||||
local function cellKey(cell)
|
||||
return cell.x .. ":" .. cell.y
|
||||
end
|
||||
|
||||
function World:isSwitchPressed(room, switch)
|
||||
local color = switch.color:getname()
|
||||
local cell = self:worldCell(room, switch:getGridPos())
|
||||
local row = cell and self.collidableMatrices[color][cell.y]
|
||||
local target = row and row[cell.x]
|
||||
local player = room.player[color]
|
||||
return target ~= nil or (player and player:occupiesCell(switch:getGridPos())) or false
|
||||
end
|
||||
|
||||
function World:rebuildCircuitMatrices()
|
||||
self.wireMatrix = {}
|
||||
self.wiredSwitchMatrix = {}
|
||||
self.wiredDoorMatrix = {}
|
||||
|
||||
local function cellAt(matrix, cell, create)
|
||||
local row = matrix[cell.y]
|
||||
if not row and create then row = {}; matrix[cell.y] = row end
|
||||
local value = row and row[cell.x]
|
||||
return value, row
|
||||
end
|
||||
local function addDevice(matrix, cell, device, room)
|
||||
local devices, row = cellAt(matrix, cell, true)
|
||||
if not devices then devices = {}; row[cell.x] = devices end
|
||||
table.insert(devices, {entity = device, room = room})
|
||||
end
|
||||
|
||||
for name, room in pairs(self.levels or {}) do
|
||||
if self:roomLocation(name) then
|
||||
for _, switch in ipairs(room.wiredSwitches or {}) do
|
||||
addDevice(self.wiredSwitchMatrix, self:worldCell(room, switch:getGridPos()), switch, room)
|
||||
end
|
||||
for _, door in ipairs(room.wiredDoors or {}) do
|
||||
-- A terminal painted beneath the door is authoritative. This also
|
||||
-- repairs older white doors whose RGB layers were assigned separate
|
||||
-- IDs before layered placement shared a circuit.
|
||||
door.circuitId = room:wireCircuitAt(door:getGridPos()) or door.circuitId
|
||||
addDevice(self.wiredDoorMatrix, self:worldCell(room, door:getGridPos()), door, room)
|
||||
end
|
||||
for y, sourceRow in pairs(room.wires or {}) do
|
||||
for x, sourceTracks in pairs(sourceRow) do
|
||||
local worldCell = self:worldCell(room, {x = x, y = y})
|
||||
local tracks, row = cellAt(self.wireMatrix, worldCell, true)
|
||||
if not tracks then tracks = {}; row[worldCell.x] = tracks end
|
||||
for circuitId, mask in pairs(sourceTracks) do
|
||||
tracks[circuitId] = {mask = mask, room = room, localCell = {x = x, y = y}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Rebuild the electrical graph from room-local fragments projected onto the
|
||||
-- world grid. IDs keep overlapping routes distinct; reciprocal direction bits
|
||||
-- are what actually make two neighbouring cells electrically continuous.
|
||||
function World:resolveCircuits()
|
||||
if not self.collidableMatrices then return end
|
||||
self:rebuildCircuitMatrices()
|
||||
local circuits = {}
|
||||
local switchesAt = {}
|
||||
local switchPressed = {}
|
||||
|
||||
for y, row in pairs(self.wiredSwitchMatrix) do
|
||||
for x, devices in pairs(row) do
|
||||
local key = cellKey({x = x, y = y})
|
||||
for _, device in ipairs(devices) do
|
||||
local switch, room = device.entity, device.room
|
||||
local pressed = self:isSwitchPressed(room, switch)
|
||||
switchPressed[switch] = pressed
|
||||
-- Entity flags are derived caches used by the existing sprite API.
|
||||
-- World.circuitState below remains the authoritative logical state.
|
||||
if pressed then switch:activate() else switch:deactivate() end
|
||||
switchesAt[key] = switchesAt[key] or {}
|
||||
table.insert(switchesAt[key], switch)
|
||||
end
|
||||
end
|
||||
end
|
||||
for y, row in pairs(self.wireMatrix) do
|
||||
for x, tracks in pairs(row) do
|
||||
local worldCell = {x = x, y = y}
|
||||
local key = cellKey(worldCell)
|
||||
for circuitId, track in pairs(tracks) do
|
||||
circuits[circuitId] = circuits[circuitId] or {}
|
||||
circuits[circuitId][key] = {cell = worldCell, mask = track.mask}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local components = {}
|
||||
for circuitId, nodes in pairs(circuits) do
|
||||
components[circuitId] = {}
|
||||
local visited = {}
|
||||
for key, node in pairs(nodes) do
|
||||
if not visited[key] then
|
||||
local component = {
|
||||
switches = {}, nodes = {}, switchColors = {red = false, green = false, blue = false},
|
||||
terminalCells = {}, valid = false,
|
||||
}
|
||||
local pending, nextNode = {node}, 1
|
||||
visited[key] = component
|
||||
while nextNode <= #pending do
|
||||
local current = pending[nextNode]
|
||||
nextNode = nextNode + 1
|
||||
local currentKey = cellKey(current.cell)
|
||||
component.nodes[currentKey] = true
|
||||
local degree = 0
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if Wire.has(current.mask, direction.bit) then
|
||||
local adjacentKey = cellKey({x = current.cell.x + direction.x, y = current.cell.y + direction.y})
|
||||
local adjacent = nodes[adjacentKey]
|
||||
if adjacent and Wire.has(adjacent.mask, direction.opposite) then
|
||||
degree = degree + 1
|
||||
if not visited[adjacentKey] then
|
||||
visited[adjacentKey] = component
|
||||
table.insert(pending, adjacent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- A switch connects only when the run literally terminates on its
|
||||
-- cell. Merely crossing or passing through a switch does not claim it.
|
||||
if degree <= 1 and switchesAt[currentKey] then
|
||||
component.terminalCells[currentKey] = switchesAt[currentKey]
|
||||
end
|
||||
end
|
||||
local terminalCount, terminalSwitches = 0
|
||||
for _, terminal in pairs(component.terminalCells) do
|
||||
terminalCount = terminalCount + 1
|
||||
terminalSwitches = terminal
|
||||
end
|
||||
-- Multiple RGB entities at one cell are one logical (possibly white)
|
||||
-- switch. Two different switch cells can never belong to one wire run.
|
||||
component.valid = terminalCount == 1
|
||||
component.terminalCount = terminalCount
|
||||
if component.valid then
|
||||
for _, switch in ipairs(terminalSwitches) do
|
||||
component.switches[switch] = true
|
||||
component.switchColors[switch.color:getname()] = true
|
||||
end
|
||||
end
|
||||
components[circuitId][key] = component
|
||||
for componentKey in pairs(component.nodes) do components[circuitId][componentKey] = component end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local doorPowered = {}
|
||||
for y, row in pairs(self.wiredDoorMatrix) do
|
||||
for x, devices in pairs(row) do
|
||||
for _, device in ipairs(devices) do
|
||||
local door = device.entity
|
||||
local position = {x = x, y = y}
|
||||
local key = cellKey(position)
|
||||
local tracks = self.wireMatrix[y] and self.wireMatrix[y][x]
|
||||
local runCount, powered = 0, tracks ~= nil
|
||||
for circuitId in pairs(tracks or {}) do
|
||||
runCount = runCount + 1
|
||||
local component = components[circuitId] and components[circuitId][key]
|
||||
if not component or not component.valid then
|
||||
powered = false
|
||||
else
|
||||
for switch in pairs(component.switches) do
|
||||
if not switchPressed[switch] then powered = false end
|
||||
end
|
||||
end
|
||||
end
|
||||
powered = powered and runCount > 0
|
||||
doorPowered[door] = powered
|
||||
-- locked/collision is a projection of world state onto the physical
|
||||
-- room entity so the existing movement renderer can consume it.
|
||||
door:setPowered(powered)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.circuits = circuits
|
||||
self.circuitComponents = components
|
||||
self.circuitState = {
|
||||
switchPressed = switchPressed,
|
||||
doorPowered = doorPowered,
|
||||
components = components,
|
||||
}
|
||||
end
|
||||
|
||||
function World:wireSwitchColorsAt(room, localCell, circuitId)
|
||||
local worldCell = self:worldCell(room, localCell)
|
||||
local component = worldCell and self.circuitComponents and
|
||||
self.circuitComponents[circuitId] and self.circuitComponents[circuitId][cellKey(worldCell)]
|
||||
if not component or not component.valid then return nil end
|
||||
local colors = component.switchColors
|
||||
if not colors.red and not colors.green and not colors.blue then return nil end
|
||||
return colors
|
||||
end
|
||||
|
||||
function World:wireColorAt(room, localCell, circuitId, visibleColors)
|
||||
local colors = self:wireSwitchColorsAt(room, localCell, circuitId)
|
||||
if not colors then return nil end
|
||||
visibleColors = visibleColors or {red = true, green = true, blue = true}
|
||||
local color = {
|
||||
colors.red and visibleColors.red and 1 or 0,
|
||||
colors.green and visibleColors.green and 1 or 0,
|
||||
colors.blue and visibleColors.blue and 1 or 0,
|
||||
}
|
||||
if color[1] == 0 and color[2] == 0 and color[3] == 0 then return nil end
|
||||
return {
|
||||
color[1], color[2], color[3],
|
||||
}
|
||||
end
|
||||
|
||||
function World:ensureWireAt(cell, circuitId)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return {} end
|
||||
local tracks = room:wireTracksAt(localCell, true)
|
||||
if tracks[circuitId] == nil then tracks[circuitId] = 0 end
|
||||
self:resolveCircuits()
|
||||
return {room}
|
||||
end
|
||||
|
||||
function World:addWireConnection(from, to, circuitId)
|
||||
local direction = Wire.directionBetween(from, to)
|
||||
if not direction then return {} end
|
||||
local fromRoom, fromCell = self:roomCell(from)
|
||||
local toRoom, toCell = self:roomCell(to)
|
||||
if not fromRoom or not toRoom then return {} end
|
||||
local fromTracks = fromRoom:wireTracksAt(fromCell, true)
|
||||
local toTracks = toRoom:wireTracksAt(toCell, true)
|
||||
local oldFrom, oldTo = fromTracks[circuitId], toTracks[circuitId]
|
||||
fromTracks[circuitId] = Wire.add(fromTracks[circuitId], direction.bit)
|
||||
toTracks[circuitId] = Wire.add(toTracks[circuitId], direction.opposite)
|
||||
self:resolveCircuits()
|
||||
local component = self.circuitComponents[circuitId] and self.circuitComponents[circuitId][cellKey(from)]
|
||||
if component and component.terminalCount > 1 then
|
||||
fromTracks[circuitId], toTracks[circuitId] = oldFrom, oldTo
|
||||
if oldFrom == nil and not next(fromTracks) then fromRoom.wires[fromCell.y][fromCell.x] = nil end
|
||||
if oldTo == nil and not next(toTracks) then toRoom.wires[toCell.y][toCell.x] = nil end
|
||||
self:resolveCircuits()
|
||||
return {}, "A wire run can terminate at only one switch. Start another run from the door."
|
||||
end
|
||||
return fromRoom == toRoom and {fromRoom} or {fromRoom, toRoom}
|
||||
end
|
||||
|
||||
function World:removeWireAt(cell, circuitId)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return {} end
|
||||
local tracks = room:wireTracksAt(localCell)
|
||||
local mask = tracks and tracks[circuitId]
|
||||
if mask == nil then return {} end
|
||||
local changed, seen = {room}, {[room] = true}
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if Wire.has(mask, direction.bit) then
|
||||
local neighborRoom, neighborCell = self:roomCell({x = cell.x + direction.x, y = cell.y + direction.y})
|
||||
local neighborTracks = neighborRoom and neighborRoom:wireTracksAt(neighborCell)
|
||||
if neighborTracks and neighborTracks[circuitId] ~= nil then
|
||||
neighborTracks[circuitId] = Wire.remove(neighborTracks[circuitId], direction.opposite)
|
||||
if not seen[neighborRoom] then seen[neighborRoom] = true; table.insert(changed, neighborRoom) end
|
||||
end
|
||||
end
|
||||
end
|
||||
room:removeWire(localCell, circuitId)
|
||||
self:resolveCircuits()
|
||||
return changed
|
||||
end
|
||||
|
||||
function World:wiredDoorAt(cell)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return nil end
|
||||
for _, door in ipairs(room.wiredDoors or {}) do
|
||||
if door:occupiesCell(localCell) then return door, room end
|
||||
end
|
||||
end
|
||||
|
||||
function World:circuitAt(cell, preferredCircuit)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
local tracks = room and room:wireTracksAt(localCell)
|
||||
if not tracks then return nil end
|
||||
if preferredCircuit and tracks[preferredCircuit] ~= nil then return preferredCircuit end
|
||||
local ids = {}
|
||||
for circuitId in pairs(tracks) do table.insert(ids, circuitId) end
|
||||
table.sort(ids)
|
||||
return ids[#ids]
|
||||
end
|
||||
|
||||
function World:movementContext(room, color)
|
||||
local proxies = {}
|
||||
local world = self
|
||||
|
|
@ -373,6 +692,8 @@ end
|
|||
function World:tick(room)
|
||||
room:tickLocal()
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
self:nextRoomCheck(room)
|
||||
end
|
||||
|
||||
|
|
@ -421,6 +742,8 @@ function World:nextRoom(room, pos)
|
|||
end
|
||||
self:playerJoinCheck(currentRoom)
|
||||
self.lastRoom = currentRoom.name
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
setEditorRoomName(currentRoom.name)
|
||||
end
|
||||
|
||||
|
|
@ -451,6 +774,7 @@ function World:moveGroup(group, direction)
|
|||
end
|
||||
self:rebuildCollision()
|
||||
for room in pairs(changed) do room:resolveSwitchesAndDoors() end
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
for room in pairs(changed) do self:markDirty(room) end
|
||||
end
|
||||
|
|
@ -499,8 +823,8 @@ function World:invalidatePreview(name)
|
|||
self.previewCache[name] = nil
|
||||
end
|
||||
|
||||
function World:drawRoomPreview(name, x, y, size)
|
||||
local room = self:getRoomData(name)
|
||||
function World:drawRoomPreview(name, x, y, size, roomData)
|
||||
local room = roomData or self:getRoomData(name)
|
||||
if not room then return end
|
||||
local scale = size / (gridWidth * 16)
|
||||
love.graphics.setScissor(x, y, size, size)
|
||||
|
|
@ -539,6 +863,77 @@ function World:drawRoomPreview(name, x, y, size)
|
|||
love.graphics.setScissor()
|
||||
end
|
||||
|
||||
function World:drawWireEditor(centerRoomX, centerRoomY, selectedCircuit)
|
||||
local roomsAcross = 3
|
||||
local roomSize = width / roomsAcross
|
||||
local cellSize = roomSize / gridWidth
|
||||
local firstRoomX, firstRoomY = centerRoomX - 1, centerRoomY - 1
|
||||
love.graphics.setColor(0.035, 0.035, 0.055, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setColor(0.28, 0.28, 0.38, 0.28)
|
||||
love.graphics.setLineWidth(1)
|
||||
for cell = 0, roomsAcross * gridWidth do
|
||||
local p = cell * cellSize
|
||||
love.graphics.line(p, 0, p, height)
|
||||
love.graphics.line(0, p, width, p)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
|
||||
for viewY = 0, roomsAcross - 1 do
|
||||
for viewX = 0, roomsAcross - 1 do
|
||||
local roomX, roomY = firstRoomX + viewX, firstRoomY + viewY
|
||||
local name = self:isInBounds(roomX, roomY) and self:roomAt(roomX, roomY)
|
||||
local sx, sy = viewX * roomSize, viewY * roomSize
|
||||
if name then
|
||||
local level = self.levels[name]
|
||||
self:drawRoomPreview(name, sx, sy, roomSize, level and level:serialize())
|
||||
if level then
|
||||
level:drawWiresAt(sx, sy, cellSize, selectedCircuit, true)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
for _, door in ipairs(level.wiredDoors or {}) do
|
||||
local pos, color = door:getGridPos(), Wire.color(door.circuitId)
|
||||
love.graphics.setColor(color[1], color[2], color[3], selectedCircuit == door.circuitId and 1 or 0.65)
|
||||
love.graphics.setLineWidth(2)
|
||||
love.graphics.circle("line", sx + (pos.x - 0.5) * cellSize, sy + (pos.y - 0.5) * cellSize, cellSize * 0.22)
|
||||
end
|
||||
for _, switch in ipairs(level.wiredSwitches or {}) do
|
||||
local pos = switch:getGridPos()
|
||||
love.graphics.setColor(1, 1, 1, switch.activated and 1 or 0.65)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.rectangle("line", sx + (pos.x - 0.68) * cellSize, sy + (pos.y - 0.68) * cellSize, cellSize * 0.36, cellSize * 0.36)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
end
|
||||
end
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setColor(name and 0.8 or 0.18, name and 0.8 or 0.18, name and 0.9 or 0.25, 1)
|
||||
love.graphics.setLineWidth(viewX == 1 and viewY == 1 and 3 or 1)
|
||||
love.graphics.rectangle("line", sx, sy, roomSize, roomSize)
|
||||
if name then
|
||||
love.graphics.setColor(1, 1, 1, 0.75)
|
||||
love.graphics.print(name, sx + 4, sy + 3)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
end
|
||||
end
|
||||
self.wireEditorProjection = {
|
||||
firstWorldX = (firstRoomX - 1) * gridWidth + 1,
|
||||
firstWorldY = (firstRoomY - 1) * gridHeight + 1,
|
||||
cellSize = cellSize,
|
||||
}
|
||||
end
|
||||
|
||||
function World:wireEditorCellAt(mouseX, mouseY, centerRoomX, centerRoomY)
|
||||
local cellSize = width / 3 / gridWidth
|
||||
local firstWorldX = (centerRoomX - 2) * gridWidth + 1
|
||||
local firstWorldY = (centerRoomY - 2) * gridHeight + 1
|
||||
return {
|
||||
x = firstWorldX + math.floor(mouseX / cellSize),
|
||||
y = firstWorldY + math.floor(mouseY / cellSize),
|
||||
}
|
||||
end
|
||||
|
||||
function World:drawHoveredRoomName()
|
||||
local mouseX, mouseY = love.mouse.getPosition()
|
||||
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue