Compare commits
3 commits
a60f46d585
...
bc4976b324
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4976b324 | ||
|
|
ee7d36e72b | ||
|
|
1965e548b4 |
16 changed files with 3026 additions and 865 deletions
|
|
@ -44,6 +44,11 @@ local worldSearchText = ""
|
||||||
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
|
local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 }
|
||||||
local cloneSourceRoom = nil
|
local cloneSourceRoom = nil
|
||||||
local roomBrush = nil
|
local roomBrush = nil
|
||||||
|
local wireBrush = nil
|
||||||
|
local selectedCircuitId = nil
|
||||||
|
local wireCameraX, wireCameraY = nil, nil
|
||||||
|
local wireDirtyRooms = {}
|
||||||
|
local wireOriginalWires = nil
|
||||||
local boxPaletteOpen = false
|
local boxPaletteOpen = false
|
||||||
local boxPaletteScroll = { scroll = 0 }
|
local boxPaletteScroll = { scroll = 0 }
|
||||||
local boxPaletteSections = {}
|
local boxPaletteSections = {}
|
||||||
|
|
@ -273,6 +278,8 @@ local function layoutRoomControls(c, s)
|
||||||
end
|
end
|
||||||
b.scale = math.min(target / spriteW, target / spriteH)
|
b.scale = math.min(target / spriteW, target / spriteH)
|
||||||
b.w, b.h = spriteW * b.scale, spriteH * b.scale
|
b.w, b.h = spriteW * b.scale, spriteH * b.scale
|
||||||
|
else
|
||||||
|
b.w, b.h = 72, target
|
||||||
end
|
end
|
||||||
return b
|
return b
|
||||||
end
|
end
|
||||||
|
|
@ -296,8 +303,8 @@ local function layoutRoomControls(c, s)
|
||||||
end
|
end
|
||||||
|
|
||||||
placeRow({ "white", "red", "green", "blue" }, 22)
|
placeRow({ "white", "red", "green", "blue" }, 22)
|
||||||
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box" }, 22)
|
placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22)
|
||||||
placeRow({ "save", "clearSaves", "world" }, 28)
|
placeRow({ "save", "clearSaves", "world", "wire" }, 28)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Build the full room-view layout (sections + a scroll list + a back/search
|
-- Build the full room-view layout (sections + a scroll list + a back/search
|
||||||
|
|
@ -492,10 +499,74 @@ local function saveRoomFromEditor()
|
||||||
gameWorld:invalidatePreview(name)
|
gameWorld:invalidatePreview(name)
|
||||||
if editorView == "world" then rebuildWorldRoomButtons() end
|
if editorView == "world" then rebuildWorldRoomButtons() end
|
||||||
currentRoom.name = name
|
currentRoom.name = name
|
||||||
|
gameWorld:setLevel(currentRoom)
|
||||||
editorRoomName = name
|
editorRoomName = name
|
||||||
roomNameField:editText(name)
|
roomNameField:editText(name)
|
||||||
activeField = nil
|
activeField = nil
|
||||||
editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or " without player positions.")
|
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
|
end
|
||||||
|
|
||||||
local function clearRuntimeSaves()
|
local function clearRuntimeSaves()
|
||||||
|
|
@ -520,6 +591,7 @@ local function clearRuntimeSaves()
|
||||||
clearSaveConfirmationScope = nil
|
clearSaveConfirmationScope = nil
|
||||||
if roomName then
|
if roomName then
|
||||||
currentRoom = Room:new{ name = roomName }
|
currentRoom = Room:new{ name = roomName }
|
||||||
|
gameWorld:setLevel(currentRoom)
|
||||||
setEditorRoomName(currentRoom.name)
|
setEditorRoomName(currentRoom.name)
|
||||||
editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'"
|
editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'"
|
||||||
else
|
else
|
||||||
|
|
@ -629,7 +701,7 @@ function _initLevelEditor()
|
||||||
}, color)
|
}, color)
|
||||||
end
|
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]
|
local properties = gameAssets[asset]
|
||||||
if properties then
|
if properties then
|
||||||
local sprites = {}
|
local sprites = {}
|
||||||
|
|
@ -648,6 +720,16 @@ function _initLevelEditor()
|
||||||
end
|
end
|
||||||
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{
|
roomNameField = createUIElement{
|
||||||
name = "roomName",
|
name = "roomName",
|
||||||
text = editorRoomName,
|
text = editorRoomName,
|
||||||
|
|
@ -837,11 +919,31 @@ end
|
||||||
function assetsView()
|
function assetsView()
|
||||||
editingAssetButtons = {}
|
editingAssetButtons = {}
|
||||||
editorView = "room"
|
editorView = "room"
|
||||||
|
wireBrush = nil
|
||||||
labels = {}
|
labels = {}
|
||||||
end
|
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()
|
function worldView()
|
||||||
editorView = "world"
|
editorView = "world"
|
||||||
|
wireBrush = nil
|
||||||
activeField = nil
|
activeField = nil
|
||||||
selectedFolder = nil
|
selectedFolder = nil
|
||||||
selectedRoom = nil
|
selectedRoom = nil
|
||||||
|
|
@ -850,6 +952,118 @@ function worldView()
|
||||||
rebuildWorldRoomButtons()
|
rebuildWorldRoomButtons()
|
||||||
end
|
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 function newWorldRoomName(cellX, cellY)
|
||||||
local base = string.format("room_%02d_%02d", cellX, cellY)
|
local base = string.format("room_%02d_%02d", cellX, cellY)
|
||||||
local name, suffix = base, 2
|
local name, suffix = base, 2
|
||||||
|
|
@ -870,6 +1084,23 @@ local function cloneWorldRoom(sourceName, cloneName)
|
||||||
editorStatus = "A level named '" .. cloneName .. "' already exists."
|
editorStatus = "A level named '" .. cloneName .. "' already exists."
|
||||||
return false
|
return false
|
||||||
end
|
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
|
if not writeToSource("rooms/" .. cloneName .. ".sav", source) then
|
||||||
editorStatus = "Could not create clone '" .. cloneName .. "'."
|
editorStatus = "Could not create clone '" .. cloneName .. "'."
|
||||||
return false
|
return false
|
||||||
|
|
@ -905,6 +1136,7 @@ local function openWorldRoom(cellX, cellY)
|
||||||
currentRoom = Room:new{ name = name }
|
currentRoom = Room:new{ name = name }
|
||||||
editorStatus = "Editing '" .. name .. ".'"
|
editorStatus = "Editing '" .. name .. ".'"
|
||||||
end
|
end
|
||||||
|
gameWorld:setLevel(currentRoom)
|
||||||
gameWorld:setLastRoom(currentRoom.name)
|
gameWorld:setLastRoom(currentRoom.name)
|
||||||
setEditorRoomName(currentRoom.name)
|
setEditorRoomName(currentRoom.name)
|
||||||
assetsView()
|
assetsView()
|
||||||
|
|
@ -958,13 +1190,17 @@ function editorMouseHandler(x, y, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- control cluster (only the buttons that live there)
|
-- 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]
|
local b = uiButtons[name]
|
||||||
if b and b:clicIsInBox(x, y) then
|
if b and b:clicIsInBox(x, y) then
|
||||||
b:onClic(button)
|
b:onClic(button)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
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
|
elseif editorView == "asset" then
|
||||||
for _, uiElement in pairs(editingAssetButtons) do
|
for _, uiElement in pairs(editingAssetButtons) do
|
||||||
if uiElement:clicIsInBox(x, y) then
|
if uiElement:clicIsInBox(x, y) then
|
||||||
|
|
@ -1002,6 +1238,8 @@ function editorMouseHandler(x, y, button)
|
||||||
-- click landed on the grid (game area): place the selected asset
|
-- click landed on the grid (game area): place the selected asset
|
||||||
if editorView == "room" then
|
if editorView == "room" then
|
||||||
beginRoomBrush(x, y, button)
|
beginRoomBrush(x, y, button)
|
||||||
|
elseif editorView == "wire" then
|
||||||
|
beginWireBrush(x, y, button)
|
||||||
elseif editorView == "world" then
|
elseif editorView == "world" then
|
||||||
local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
|
local cellX, cellY = gameWorld:getCellFromMousePos(x, y)
|
||||||
if not gameWorld:isInBounds(cellX, cellY) then return end
|
if not gameWorld:isInBounds(cellX, cellY) then return end
|
||||||
|
|
@ -1024,6 +1262,15 @@ function editorMouseHandler(x, y, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorMouseMoved(x, y)
|
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 not roomBrush then return end
|
||||||
if editorView ~= "room" then
|
if editorView ~= "room" then
|
||||||
roomBrush = nil
|
roomBrush = nil
|
||||||
|
|
@ -1045,6 +1292,7 @@ end
|
||||||
|
|
||||||
function editorMouseReleased(_, _, button)
|
function editorMouseReleased(_, _, button)
|
||||||
if roomBrush and roomBrush.button == button then roomBrush = nil end
|
if roomBrush and roomBrush.button == button then roomBrush = nil end
|
||||||
|
if wireBrush and wireBrush.button == button then wireBrush = nil end
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorTextHandler(text)
|
function editorTextHandler(text)
|
||||||
|
|
@ -1094,6 +1342,7 @@ function editorCompleteResponse()
|
||||||
end
|
end
|
||||||
|
|
||||||
function editorCancelResponse()
|
function editorCancelResponse()
|
||||||
|
if editorView == "wire" then discardWireRooms(); return true end
|
||||||
if editorView ~= "world" or activeField ~= "cloneName" then return false end
|
if editorView ~= "world" or activeField ~= "cloneName" then return false end
|
||||||
activeField = nil
|
activeField = nil
|
||||||
cloneSourceRoom = nil
|
cloneSourceRoom = nil
|
||||||
|
|
@ -1153,11 +1402,15 @@ function editorDraw()
|
||||||
-- control cluster
|
-- control cluster
|
||||||
love.graphics.setColor(gColor.white:set())
|
love.graphics.setColor(gColor.white:set())
|
||||||
love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center")
|
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]
|
local button = uiButtons[name]
|
||||||
if button then
|
if button then
|
||||||
button:draw()
|
button:draw()
|
||||||
local asset = name:match("^quick_(.+)$")
|
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
|
if asset and selectedAsset == asset then
|
||||||
love.graphics.setColor(1, 0.85, 0.2, 1)
|
love.graphics.setColor(1, 0.85, 0.2, 1)
|
||||||
love.graphics.setLineWidth(2)
|
love.graphics.setLineWidth(2)
|
||||||
|
|
@ -1173,6 +1426,32 @@ function editorDraw()
|
||||||
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
|
love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center")
|
||||||
Layout.unclip()
|
Layout.unclip()
|
||||||
end
|
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
|
elseif editorView == "asset" then
|
||||||
for _, button in pairs(editingAssetButtons) do
|
for _, button in pairs(editingAssetButtons) do
|
||||||
button:draw()
|
button:draw()
|
||||||
|
|
@ -1186,7 +1465,7 @@ function editorDraw()
|
||||||
end
|
end
|
||||||
-- The room view draws its status inside its own strip (see the room branch);
|
-- The room view draws its status inside its own strip (see the room branch);
|
||||||
-- the other views keep the simple centered line.
|
-- 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.setColor(gColor.white:set())
|
||||||
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
|
love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,23 @@ function getAllAssets()
|
||||||
end
|
end
|
||||||
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)
|
return BoxVariants.addTo(globalAssetProperties)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
39
main.lua
39
main.lua
|
|
@ -35,6 +35,9 @@ function love.load()
|
||||||
_initAccessibility()
|
_initAccessibility()
|
||||||
gameWorld = World:new()
|
gameWorld = World:new()
|
||||||
_initLevelEditor()
|
_initLevelEditor()
|
||||||
|
-- getAllAssets runs in _initLevelEditor; deserialize the world only after
|
||||||
|
-- its asset metadata exists, otherwise every non-player object is skipped.
|
||||||
|
gameWorld:loadLevels()
|
||||||
|
|
||||||
_initCanvases()
|
_initCanvases()
|
||||||
|
|
||||||
|
|
@ -43,10 +46,16 @@ function love.load()
|
||||||
-- Resume the game: prefer the runtime autosave so the last saved position is
|
-- Resume the game: prefer the runtime autosave so the last saved position is
|
||||||
-- restored, falling back to the authored source level. Editor room-opens go
|
-- restored, falling back to the authored source level. Editor room-opens go
|
||||||
-- through their own (source-only) load path, so this only affects resume.
|
-- through their own (source-only) load path, so this only affects resume.
|
||||||
currentRoom = Room:new{
|
local initialRoom = gameWorld.lastRoom or "start"
|
||||||
name = gameWorld.lastRoom or "start",
|
-- Preserve the old resume behavior for the active room while the rest of the
|
||||||
runtime = true
|
-- world comes from the authored level files.
|
||||||
}
|
currentRoom = Room:new{name = initialRoom, runtime = true}
|
||||||
|
if gameWorld.levels[initialRoom] then
|
||||||
|
gameWorld.levels[initialRoom] = currentRoom
|
||||||
|
gameWorld:rebuildCollision()
|
||||||
|
gameWorld:resolveCircuits()
|
||||||
|
gameWorld:rebuildCollision()
|
||||||
|
end
|
||||||
gameWorld:setLastRoom(currentRoom.name)
|
gameWorld:setLastRoom(currentRoom.name)
|
||||||
setEditorRoomName(currentRoom.name)
|
setEditorRoomName(currentRoom.name)
|
||||||
|
|
||||||
|
|
@ -59,17 +68,21 @@ function love.quit()
|
||||||
-- An explicit editor save/clear removes the runtime copy intentionally;
|
-- An explicit editor save/clear removes the runtime copy intentionally;
|
||||||
-- don't immediately recreate it while shutting down.
|
-- don't immediately recreate it while shutting down.
|
||||||
if currentRoom and not suppressRuntimeAutosave then
|
if currentRoom and not suppressRuntimeAutosave then
|
||||||
|
if gameWorld then
|
||||||
|
gameWorld:markDirty(currentRoom)
|
||||||
|
gameWorld:flushRuntime()
|
||||||
|
else
|
||||||
currentRoom:save()
|
currentRoom:save()
|
||||||
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
if editorView == "world" then
|
if editorView == "world" or editorView == "wire" then
|
||||||
Input:stopMoveRepeat()
|
Input:stopMoveRepeat()
|
||||||
else
|
else
|
||||||
local repeatedMove = Input:handler(dt)
|
local repeatedMove = Input:handler(dt)
|
||||||
if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end
|
if repeatedMove and not Dialogue.isOpen() then gameWorld:movePlayer(currentRoom, repeatedMove) end
|
||||||
end
|
end
|
||||||
currentRoom:update(dt)
|
currentRoom:update(dt)
|
||||||
|
|
||||||
|
|
@ -83,6 +96,8 @@ function love.draw()
|
||||||
love.graphics.setBlendMode("screen")
|
love.graphics.setBlendMode("screen")
|
||||||
if editorView == "world" then
|
if editorView == "world" then
|
||||||
gameWorld:draw()
|
gameWorld:draw()
|
||||||
|
elseif editorView == "wire" then
|
||||||
|
gameWorld:drawWireEditor(wireEditorCamera())
|
||||||
else
|
else
|
||||||
currentRoom:draw()
|
currentRoom:draw()
|
||||||
end
|
end
|
||||||
|
|
@ -162,12 +177,14 @@ function love.keypressed(key)
|
||||||
return
|
return
|
||||||
end
|
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)
|
Input:beginMove(key)
|
||||||
currentRoom:movePlayer(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)
|
local c = Input.playerColor(key)
|
||||||
currentRoom:switchPlayerColor(c)
|
gameWorld:switchPlayerColor(currentRoom, c)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- if key == "w" then
|
-- if key == "w" then
|
||||||
|
|
|
||||||
303
room.lua
303
room.lua
|
|
@ -8,6 +8,8 @@ require "art"
|
||||||
require "assorted"
|
require "assorted"
|
||||||
require "switch"
|
require "switch"
|
||||||
require "door"
|
require "door"
|
||||||
|
require "wired_switch"
|
||||||
|
require "wired_door"
|
||||||
require "dialogue_manager"
|
require "dialogue_manager"
|
||||||
require "generic_moveable"
|
require "generic_moveable"
|
||||||
require "generic_sticky"
|
require "generic_sticky"
|
||||||
|
|
@ -17,11 +19,15 @@ require "generic_unmoveable"
|
||||||
require "generic_player_blocker"
|
require "generic_player_blocker"
|
||||||
|
|
||||||
local Movement = require "movement"
|
local Movement = require "movement"
|
||||||
|
local Wire = require "wire"
|
||||||
|
|
||||||
function Room:initialize(t)
|
function Room:initialize(t)
|
||||||
self.player = {}
|
self.player = {}
|
||||||
self.switches = { red = {}, green = {}, blue = {} }
|
self.switches = { red = {}, green = {}, blue = {} }
|
||||||
self.doors = {}
|
self.doors = {}
|
||||||
|
self.wiredSwitches = {}
|
||||||
|
self.wiredDoors = {}
|
||||||
|
self.wires = {}
|
||||||
self.collideables = { red = {}, green = {}, blue = {} }
|
self.collideables = { red = {}, green = {}, blue = {} }
|
||||||
self.playerBlockers = { red = {}, green = {}, blue = {} }
|
self.playerBlockers = { red = {}, green = {}, blue = {} }
|
||||||
self.npcs = {}
|
self.npcs = {}
|
||||||
|
|
@ -125,6 +131,7 @@ function Room:update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:draw()
|
function Room:draw()
|
||||||
|
self:drawWiresAt(0, 0, width / gridWidth, nil, false)
|
||||||
|
|
||||||
-- draw only the channels the player has. Each object just exposes sprites per
|
-- 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
|
-- channel via Entity:draw(channel); a door registered on all three channels
|
||||||
|
|
@ -132,12 +139,120 @@ function Room:draw()
|
||||||
for _, channel in ipairs(self.colorsPlayerHas) do
|
for _, channel in ipairs(self.colorsPlayerHas) do
|
||||||
if self.player[channel] then self.player[channel]:draw(channel) end
|
if self.player[channel] then self.player[channel]:draw(channel) end
|
||||||
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
|
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
|
||||||
|
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 _, 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 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
|
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
|
||||||
end
|
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()
|
function Room:createCollideablesMatrix()
|
||||||
for color, entities in pairs(self.collideables) do
|
for color, entities in pairs(self.collideables) do
|
||||||
self.collidableMatrices[color] = self:createEntityMatrix(entities)
|
self.collidableMatrices[color] = self:createEntityMatrix(entities)
|
||||||
|
|
@ -181,7 +296,7 @@ function Room:createEntityMatrix(entities)
|
||||||
for _, entity in pairs(entities) do
|
for _, entity in pairs(entities) do
|
||||||
for _, occupiedCells in pairs(entity:getOccupiedCells()) do
|
for _, occupiedCells in pairs(entity:getOccupiedCells()) do
|
||||||
--we store a table reference so we can call a method on it during a chec_
|
--we store a table reference so we can call a method on it during a chec_
|
||||||
matrix[occupiedCells.y][occupiedCells.x] = entity
|
if self:isInBounds(occupiedCells) then matrix[occupiedCells.y][occupiedCells.x] = entity end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- printMatrix(matrix)
|
-- printMatrix(matrix)
|
||||||
|
|
@ -193,7 +308,9 @@ function Room:getCellInMatrix(matrix, cell)
|
||||||
return matrix[cell.y][cell.x]
|
return matrix[cell.y][cell.x]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:movePlayer(key)
|
-- Kept for isolated room tests/editor previews. The live game goes through
|
||||||
|
-- World:movePlayer so no gameplay decision is room-local.
|
||||||
|
function Room:movePlayerLocal(key)
|
||||||
|
|
||||||
local moved = false
|
local moved = false
|
||||||
local previousPositions = {} --these are so we can perform the overlap chec_
|
local previousPositions = {} --these are so we can perform the overlap chec_
|
||||||
|
|
@ -224,23 +341,36 @@ function Room:movePlayer(key)
|
||||||
x = pos.x + dir.x, y = pos.y + dir.y
|
x = pos.x + dir.x, y = pos.y + dir.y
|
||||||
} -- this is for reference for each color as we change the targetCells table
|
} -- this is for reference for each color as we change the targetCells table
|
||||||
|
|
||||||
|
local useWorld = gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self
|
||||||
|
local context = useWorld and gameWorld:movementContext(self, color)
|
||||||
|
local worldTarget = useWorld and context:worldCell(initialTargetCell)
|
||||||
local initialTargetEntity
|
local initialTargetEntity
|
||||||
|
|
||||||
if self:isInBounds(initialTargetCell) then
|
if useWorld then
|
||||||
|
initialTargetEntity = context:entityAt(worldTarget)
|
||||||
|
elseif self:isInBounds(initialTargetCell) then
|
||||||
initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
|
initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
|
||||||
end
|
end
|
||||||
|
|
||||||
if initialTargetEntity then initialTargetEntity:bump() end
|
if initialTargetEntity then initialTargetEntity:bump() end
|
||||||
local playerBlocker
|
local playerBlocker
|
||||||
if self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then
|
if useWorld then
|
||||||
|
playerBlocker = context:playerBlockerAt(worldTarget)
|
||||||
|
elseif self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then
|
||||||
playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell)
|
playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self:isInBounds(initialTargetCell) then
|
if useWorld and not context:isInBounds(worldTarget) then
|
||||||
|
move = true
|
||||||
|
elseif not useWorld and not self:isInBounds(initialTargetCell) then
|
||||||
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
||||||
move = true
|
move = true
|
||||||
elseif playerBlocker and playerBlocker:blocksPlayer() then
|
elseif playerBlocker and playerBlocker:blocksPlayer() then
|
||||||
move = false
|
move = false
|
||||||
|
elseif useWorld then
|
||||||
|
move, entitiesToPush = Movement.findMovableGroup({worldTarget}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
else
|
else
|
||||||
move, entitiesToPush = Movement.findMovableGroup(
|
move, entitiesToPush = Movement.findMovableGroup(
|
||||||
{initialTargetCell}, dir,
|
{initialTargetCell}, dir,
|
||||||
|
|
@ -253,7 +383,11 @@ function Room:movePlayer(key)
|
||||||
|
|
||||||
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
||||||
local pull = false
|
local pull = false
|
||||||
if self:isInBounds(initialTargetCell) then
|
if useWorld and context:isInBounds(context:worldCell(initialTargetCell)) then
|
||||||
|
pull, entitiesToPull = Movement.findMovableGroup({context:worldCell(initialTargetCell)}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
elseif not useWorld and self:isInBounds(initialTargetCell) then
|
||||||
pull, entitiesToPull = Movement.findMovableGroup(
|
pull, entitiesToPull = Movement.findMovableGroup(
|
||||||
{initialTargetCell}, dir,
|
{initialTargetCell}, dir,
|
||||||
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
||||||
|
|
@ -262,10 +396,15 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
if move then
|
if move then
|
||||||
for _, entity in pairs(entitiesToPush) do
|
if useWorld then
|
||||||
entity:move(axis, dir[axis])
|
local group = {}
|
||||||
|
for entity in pairs(entitiesToPush) do group[entity] = entity end
|
||||||
|
if pull then for entity in pairs(entitiesToPull) do group[entity] = entity end end
|
||||||
|
gameWorld:moveGroup(group, dir)
|
||||||
|
else
|
||||||
|
for _, entity in pairs(entitiesToPush) do entity:move(axis, dir[axis]) end
|
||||||
end
|
end
|
||||||
if pull == true then
|
if pull == true and not useWorld then
|
||||||
for _, entity in pairs(entitiesToPull) do
|
for _, entity in pairs(entitiesToPull) do
|
||||||
-- A connected component can wrap around the player and be found by
|
-- A connected component can wrap around the player and be found by
|
||||||
-- both searches. Every entity still moves exactly one grid cell.
|
-- both searches. Every entity still moves exactly one grid cell.
|
||||||
|
|
@ -281,12 +420,20 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
if moved then
|
if moved then
|
||||||
self:playerJoinCheck(previousPositions, newPositions)
|
if useWorld then gameWorld:playerJoinCheck(self, previousPositions, newPositions)
|
||||||
|
else self:playerJoinCheck(previousPositions, newPositions) end
|
||||||
self:tick()
|
self:tick()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:switchPlayerColor(color)
|
function Room:movePlayer(key)
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
return gameWorld:movePlayer(self, key)
|
||||||
|
end
|
||||||
|
return self:movePlayerLocal(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Room:switchPlayerColorLocal(color)
|
||||||
-- Number keys can request a colour the player has not collected yet. Do
|
-- Number keys can request a colour the player has not collected yet. Do
|
||||||
-- nothing in that case: there is no corresponding player object to switch
|
-- nothing in that case: there is no corresponding player object to switch
|
||||||
-- to (or use as the sound source).
|
-- to (or use as the sound source).
|
||||||
|
|
@ -304,6 +451,13 @@ function Room:switchPlayerColor(color)
|
||||||
return canSwitch
|
return canSwitch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Room:switchPlayerColor(color)
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
return gameWorld:switchPlayerColor(self, color)
|
||||||
|
end
|
||||||
|
return self:switchPlayerColorLocal(color)
|
||||||
|
end
|
||||||
|
|
||||||
function Room:setActiveColor(color, bool)
|
function Room:setActiveColor(color, bool)
|
||||||
self.activeColors[color] = bool
|
self.activeColors[color] = bool
|
||||||
end
|
end
|
||||||
|
|
@ -361,7 +515,7 @@ function Room:onlyColorIsActive(color)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:tick()
|
function Room:tickLocal()
|
||||||
for color, entityArray in pairs(self.collideables) do
|
for color, entityArray in pairs(self.collideables) do
|
||||||
for _, entity in pairs(entityArray) do
|
for _, entity in pairs(entityArray) do
|
||||||
entity:tick()
|
entity:tick()
|
||||||
|
|
@ -371,6 +525,11 @@ function Room:tick()
|
||||||
self:createPlayerBlockerMatrix()
|
self:createPlayerBlockerMatrix()
|
||||||
self:createSwitchMatrix()
|
self:createSwitchMatrix()
|
||||||
self:resolveSwitchesAndDoors()
|
self:resolveSwitchesAndDoors()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Room:tick()
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then return gameWorld:tick(self) end
|
||||||
|
self:tickLocal()
|
||||||
self:nextRoomCheck()
|
self:nextRoomCheck()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -445,7 +604,14 @@ function Room:switchCheck()
|
||||||
for color, switchArray in pairs(self.switches) do
|
for color, switchArray in pairs(self.switches) do
|
||||||
local onSwitches = {}
|
local onSwitches = {}
|
||||||
for _, switch in pairs(switchArray) do
|
for _, switch in pairs(switchArray) do
|
||||||
local targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
|
local targetCell
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
local cell = gameWorld:worldCell(self, switch:getGridPos())
|
||||||
|
local row = cell and gameWorld.collidableMatrices[color][cell.y]
|
||||||
|
targetCell = row and row[cell.x]
|
||||||
|
else
|
||||||
|
targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
|
||||||
|
end
|
||||||
local player = self.player[color]
|
local player = self.player[color]
|
||||||
-- Players are not stored in collidableMatrices, but a colour copy
|
-- Players are not stored in collidableMatrices, but a colour copy
|
||||||
-- standing on its matching switch presses it just like a block does.
|
-- standing on its matching switch presses it just like a block does.
|
||||||
|
|
@ -486,7 +652,8 @@ end
|
||||||
-- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/
|
-- 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
|
-- "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.
|
-- 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
|
if class == "npc" then
|
||||||
local entity = Sign:new(x, y)
|
local entity = Sign:new(x, y)
|
||||||
self:registerNpc(entity)
|
self:registerNpc(entity)
|
||||||
|
|
@ -502,7 +669,31 @@ function Room:createEntity(class, color, x, y)
|
||||||
local behavior = props and props.class or "immoveable"
|
local behavior = props and props.class or "immoveable"
|
||||||
local entity
|
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
|
-- Old migrated rooms stored a colourless shared door. Expand that legacy
|
||||||
-- record into ordinary colour-layer door objects on load.
|
-- record into ordinary colour-layer door objects on load.
|
||||||
if not color then
|
if not color then
|
||||||
|
|
@ -570,6 +761,15 @@ function Room:registerSwitch(entityPointer, color)
|
||||||
table.insert(self.switches[color], entityPointer)
|
table.insert(self.switches[color], entityPointer)
|
||||||
end
|
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)
|
function Room:registerPlayer(entityPointer, color)
|
||||||
table.insert(self.colorsPlayerHas, color)
|
table.insert(self.colorsPlayerHas, color)
|
||||||
self.player[color] = entityPointer
|
self.player[color] = entityPointer
|
||||||
|
|
@ -639,6 +839,15 @@ function Room:attemptDelete(mouseX, mouseY, color)
|
||||||
for index = #self.doors, 1, -1 do
|
for index = #self.doors, 1, -1 do
|
||||||
if removedEntities[self.doors[index]] then table.remove(self.doors, index) end
|
if removedEntities[self.doors[index]] then table.remove(self.doors, index) end
|
||||||
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
|
-- NPCs are stamped into every collision layer, but are kept in their own
|
||||||
-- registry rather than in self.collideables. Remove them directly so a
|
-- registry rather than in self.collideables. Remove them directly so a
|
||||||
-- right-click works no matter which editor colour is currently selected.
|
-- right-click works no matter which editor colour is currently selected.
|
||||||
|
|
@ -694,7 +903,7 @@ end
|
||||||
|
|
||||||
function Room:deserialize(saveTable)
|
function Room:deserialize(saveTable)
|
||||||
for _, obj in ipairs(saveTable.objects or {}) do
|
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
|
-- 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
|
-- copies that were off at save time stay off (older savs lack the field and
|
||||||
-- keep the old all-on behaviour).
|
-- keep the old all-on behaviour).
|
||||||
|
|
@ -702,6 +911,11 @@ function Room:deserialize(saveTable)
|
||||||
self:setActiveColor(obj.color, obj.active)
|
self:setActiveColor(obj.color, obj.active)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
function Room:save()
|
function Room:save()
|
||||||
|
|
@ -722,12 +936,43 @@ function Room:saveToSource(name, savePlayerPositions)
|
||||||
return writeToSource("rooms/" .. name .. ".sav", room)
|
return writeToSource("rooms/" .. name .. ".sav", room)
|
||||||
end
|
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)
|
function Room:serialize(includePlayers)
|
||||||
local objects = {}
|
local objects = {}
|
||||||
|
|
||||||
local function emit(class, color, entity)
|
local function emit(class, color, entity)
|
||||||
local pos = entity:getGridPos()
|
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
|
end
|
||||||
|
|
||||||
if includePlayers ~= false then
|
if includePlayers ~= false then
|
||||||
|
|
@ -760,15 +1005,19 @@ function Room:serialize(includePlayers)
|
||||||
emit("switch", color, switch)
|
emit("switch", color, switch)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
for _, switch in ipairs(self.wiredSwitches) do
|
||||||
|
emit("wired_switch", switch.color:getname(), switch)
|
||||||
|
end
|
||||||
|
|
||||||
for _, npc in ipairs(self.npcs) do
|
for _, npc in ipairs(self.npcs) do
|
||||||
table.insert(objects, { class = npc.saveClass or "npc", x = npc.x, y = npc.y })
|
table.insert(objects, { class = npc.saveClass or "npc", x = npc.x, y = npc.y })
|
||||||
end
|
end
|
||||||
|
|
||||||
return { objects = objects }
|
return { objects = objects, wires = self:serializeWires() }
|
||||||
end
|
end
|
||||||
|
|
||||||
function nextRoom(pos)
|
function nextRoom(pos)
|
||||||
|
if gameWorld and currentRoom then return gameWorld:nextRoom(currentRoom, pos) end
|
||||||
local exit
|
local exit
|
||||||
local newPos = {x = pos.x, y = pos.y}
|
local newPos = {x = pos.x, y = pos.y}
|
||||||
if pos.x > gridWidth then
|
if pos.x > gridWidth then
|
||||||
|
|
@ -791,13 +1040,17 @@ function nextRoom(pos)
|
||||||
|
|
||||||
-- Missing map neighbours intentionally wrap back into the room just exited.
|
-- Missing map neighbours intentionally wrap back into the room just exited.
|
||||||
-- A placed neighbour gets the same carried player state at its opposite edge.
|
-- A placed neighbour gets the same carried player state at its opposite edge.
|
||||||
currentRoom = Room:new{
|
local playerState = {x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()}
|
||||||
name = nextName or currentName,
|
currentRoom = gameWorld and gameWorld.levels[nextName or currentName] or Room:new{name = nextName or currentName, runtime = true}
|
||||||
runtime = true,
|
if currentRoom then
|
||||||
player = {
|
currentRoom.activeColors = {red = false, green = false, blue = false}
|
||||||
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
|
for _, color in ipairs(playerState.activeColors) do
|
||||||
}
|
if currentRoom.player[color] then currentRoom.player[color].x, currentRoom.player[color].y = playerState.x, playerState.y
|
||||||
}
|
else currentRoom:registerPlayer(Player:new(playerState.x, playerState.y, color), color) end
|
||||||
|
currentRoom:setActiveColor(color, true)
|
||||||
|
end
|
||||||
|
gameWorld:playerJoinCheck(currentRoom)
|
||||||
|
end
|
||||||
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
||||||
if currentRoom.name then setEditorRoomName(currentRoom.name) end
|
if currentRoom.name then setEditorRoomName(currentRoom.name) end
|
||||||
|
|
||||||
|
|
|
||||||
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={
|
objects={
|
||||||
{
|
{
|
||||||
color="blue",
|
|
||||||
class="door",
|
class="door",
|
||||||
|
color="blue",
|
||||||
x=6,
|
x=6,
|
||||||
y=1
|
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",
|
class="door",
|
||||||
|
color="red",
|
||||||
x=6,
|
x=6,
|
||||||
y=1
|
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",
|
class="door",
|
||||||
|
color="green",
|
||||||
x=6,
|
x=6,
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color="blue",
|
|
||||||
class="static_wall",
|
class="static_wall",
|
||||||
x=1,
|
|
||||||
y=1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="red",
|
|
||||||
class="static_wall",
|
|
||||||
x=1,
|
|
||||||
y=1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="green",
|
color="green",
|
||||||
class="static_wall",
|
|
||||||
x=1,
|
x=1,
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color="blue",
|
|
||||||
class="static_wall",
|
class="static_wall",
|
||||||
|
color="green",
|
||||||
x=2,
|
x=2,
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color="red",
|
|
||||||
class="static_wall",
|
class="static_wall",
|
||||||
x=2,
|
|
||||||
y=1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="green",
|
color="green",
|
||||||
class="static_wall",
|
|
||||||
x=2,
|
|
||||||
y=1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="blue",
|
|
||||||
class="static_wall",
|
|
||||||
x=3,
|
x=3,
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color="green",
|
|
||||||
class="static_wall",
|
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,
|
x=3,
|
||||||
y=1
|
y=6
|
||||||
},
|
|
||||||
{
|
|
||||||
color="red",
|
|
||||||
class="static_wall",
|
|
||||||
x=3,
|
|
||||||
y=1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="red",
|
|
||||||
class="static_wall",
|
|
||||||
x=4,
|
|
||||||
y=1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
class="spiky_box_2x1_11",
|
||||||
color="green",
|
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,
|
x=1,
|
||||||
y=11
|
y=6
|
||||||
},
|
|
||||||
{
|
|
||||||
color="blue",
|
|
||||||
class="static_wall",
|
|
||||||
x=1,
|
|
||||||
y=11
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
class="box",
|
||||||
color="green",
|
color="green",
|
||||||
class="static_wall",
|
x=5,
|
||||||
x=1,
|
y=6
|
||||||
y=11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="red",
|
|
||||||
class="static_wall",
|
|
||||||
x=11,
|
|
||||||
y=11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="blue",
|
|
||||||
class="static_wall",
|
|
||||||
x=11,
|
|
||||||
y=11
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
class="box_1x2_1-1",
|
||||||
color="green",
|
color="green",
|
||||||
class="static_wall",
|
|
||||||
x=11,
|
|
||||||
y=11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color="red",
|
|
||||||
class="switch",
|
|
||||||
x=6,
|
x=6,
|
||||||
y=6
|
y=6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color="blue",
|
class="spiky_box_1x2_1-1",
|
||||||
class="switch",
|
color="green",
|
||||||
x=6,
|
x=7,
|
||||||
y=6
|
y=6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
class="box_2x2_11-10",
|
||||||
color="green",
|
color="green",
|
||||||
class="switch",
|
x=8,
|
||||||
x=6,
|
y=5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class="spiky_box_2x1_11",
|
||||||
|
color="green",
|
||||||
|
x=9,
|
||||||
|
y=6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
class="spiky_box_2x1_11",
|
||||||
|
color="green",
|
||||||
|
x=11,
|
||||||
y=6
|
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={
|
rooms={
|
||||||
{
|
{
|
||||||
name="3",
|
|
||||||
x=10,
|
x=10,
|
||||||
|
name="3",
|
||||||
y=10
|
y=10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=12,
|
||||||
name="5",
|
name="5",
|
||||||
x=12,
|
|
||||||
y=10
|
y=10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="village_1",
|
|
||||||
x=12,
|
x=12,
|
||||||
|
name="village_1",
|
||||||
y=9
|
y=9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="village_3",
|
x=11,
|
||||||
x=13,
|
|
||||||
y=8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name="village_4",
|
name="village_4",
|
||||||
x=11,
|
|
||||||
y=8
|
y=8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_1",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="yellow_1",
|
||||||
y=7
|
y=7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_3",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="yellow_3",
|
||||||
y=6
|
y=6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_2",
|
|
||||||
x=12,
|
x=12,
|
||||||
|
name="yellow_2",
|
||||||
y=7
|
y=7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_4",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="yellow_4",
|
||||||
y=5
|
y=5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_5",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="yellow_5",
|
||||||
y=4
|
y=4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_6",
|
|
||||||
x=10,
|
x=10,
|
||||||
|
name="yellow_6",
|
||||||
y=5
|
y=5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=1,
|
||||||
name="default",
|
name="default",
|
||||||
x=1,
|
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=3,
|
||||||
name="old_yellow_3",
|
name="old_yellow_3",
|
||||||
x=3,
|
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="test_patt",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="test_patt",
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=7,
|
||||||
name="test_patt_update",
|
name="test_patt_update",
|
||||||
x=7,
|
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=9,
|
||||||
name="white_asymmetrical",
|
name="white_asymmetrical",
|
||||||
x=9,
|
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="white_ok",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="white_ok",
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="white_primary",
|
|
||||||
x=13,
|
x=13,
|
||||||
|
name="white_primary",
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_arches",
|
|
||||||
x=15,
|
x=15,
|
||||||
|
name="yellow_arches",
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_asymmetrical",
|
|
||||||
x=17,
|
x=17,
|
||||||
|
name="yellow_asymmetrical",
|
||||||
y=1
|
y=1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_cc_moustache",
|
|
||||||
x=1,
|
x=1,
|
||||||
|
name="yellow_cc_moustache",
|
||||||
y=3
|
y=3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_color_changing_levvel_1",
|
|
||||||
x=3,
|
x=3,
|
||||||
|
name="yellow_color_changing_levvel_1",
|
||||||
y=3
|
y=3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_smile",
|
|
||||||
x=7,
|
x=7,
|
||||||
|
name="yellow_smile",
|
||||||
y=3
|
y=3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="2",
|
|
||||||
x=9,
|
x=9,
|
||||||
|
name="2",
|
||||||
y=10
|
y=10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="room_11_09",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="room_11_09",
|
||||||
y=9
|
y=9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="yellow_skull",
|
|
||||||
x=9,
|
x=9,
|
||||||
|
name="yellow_skull",
|
||||||
y=3
|
y=3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="jorge_room",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="jorge_room",
|
||||||
y=5
|
y=5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="4",
|
|
||||||
x=11,
|
x=11,
|
||||||
|
name="4",
|
||||||
y=10
|
y=10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="room_01_05",
|
|
||||||
x=1,
|
x=1,
|
||||||
|
name="room_01_05",
|
||||||
y=5
|
y=5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="ok",
|
|
||||||
x=2,
|
x=2,
|
||||||
|
name="ok",
|
||||||
y=8
|
y=8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="room_05_18",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="room_05_18",
|
||||||
y=18
|
y=18
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="village_start",
|
|
||||||
x=9,
|
x=9,
|
||||||
|
name="village_start",
|
||||||
y=11
|
y=11
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="spiky_test",
|
|
||||||
x=4,
|
x=4,
|
||||||
|
name="spiky_test",
|
||||||
y=8
|
y=8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
x=5,
|
||||||
name="corner_trimino_test",
|
name="corner_trimino_test",
|
||||||
x=5,
|
|
||||||
y=7
|
y=7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="tetromino_loop_t",
|
|
||||||
x=6,
|
x=6,
|
||||||
|
name="tetromino_loop_t",
|
||||||
y=7
|
y=7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="corner_trimino_test_copy",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="corner_trimino_test_copy",
|
||||||
y=6
|
y=6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="blocker_test",
|
|
||||||
x=7,
|
x=7,
|
||||||
|
name="blocker_test",
|
||||||
y=7
|
y=7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="spiky_test_2",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="spiky_test_2",
|
||||||
y=8
|
y=8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="spiky_test_3",
|
|
||||||
x=5,
|
x=5,
|
||||||
|
name="spiky_test_3",
|
||||||
y=9
|
y=9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="jorge2",
|
|
||||||
x=3,
|
x=3,
|
||||||
|
name="jorge2",
|
||||||
y=14
|
y=14
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name="room_08_07",
|
|
||||||
x=8,
|
x=8,
|
||||||
|
name="room_08_07",
|
||||||
y=7
|
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
|
||||||
761
world.lua
761
world.lua
|
|
@ -1,8 +1,11 @@
|
||||||
World = class("World")
|
World = class("World")
|
||||||
|
|
||||||
-- The world is deliberately only a layout: rooms continue to own their puzzle
|
local Movement = require "movement"
|
||||||
-- state. Keeping the two separate lets the editor move a room without
|
local Wire = require "wire"
|
||||||
-- 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()
|
function World:initialize()
|
||||||
self.width, self.height = 18, 18
|
self.width, self.height = 18, 18
|
||||||
self.rooms = {}
|
self.rooms = {}
|
||||||
|
|
@ -10,6 +13,72 @@ function World:initialize()
|
||||||
self.previewCache = {}
|
self.previewCache = {}
|
||||||
self.lastRoom = nil
|
self.lastRoom = nil
|
||||||
self:load()
|
self:load()
|
||||||
|
self:loadRuntimeState()
|
||||||
|
self.levels = {}
|
||||||
|
self.circuitSerial = 0
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:loadLevels()
|
||||||
|
-- Gameplay saves go to LÖVE's runtime directory. Seed every world slice
|
||||||
|
-- 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
|
||||||
|
-- global simulation pointing at that exact instance, not a stale preload.
|
||||||
|
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.
|
||||||
|
function World:loadRuntimeState()
|
||||||
|
local raw = love.filesystem.read("rooms/world.sav")
|
||||||
|
if not raw then return end
|
||||||
|
local ok, state = pcall(TSerial.unpack, raw)
|
||||||
|
if ok and type(state) == "table" and state.lastRoom and self:roomLocation(state.lastRoom) then
|
||||||
|
self.lastRoom = state.lastRoom
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:saveRuntime(dirtyRooms)
|
||||||
|
for room in pairs(dirtyRooms or {}) do room:save() end
|
||||||
|
return love.filesystem.write("rooms/world.sav", TSerial.pack({lastRoom = currentRoom and currentRoom.name}, nil, true))
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:markDirty(room)
|
||||||
|
self.dirtyRooms = self.dirtyRooms or {}
|
||||||
|
self.dirtyRooms[room] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:flushRuntime()
|
||||||
|
self:saveRuntime(self.dirtyRooms)
|
||||||
|
self.dirtyRooms = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function World:listRoomNames()
|
function World:listRoomNames()
|
||||||
|
|
@ -80,7 +149,13 @@ end
|
||||||
function World:placeRoom(name, x, y)
|
function World:placeRoom(name, x, y)
|
||||||
if not name or not self:isInBounds(x, y) then return false end
|
if not name or not self:isInBounds(x, y) then return false end
|
||||||
self:_putRoom(name, x, y)
|
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
|
end
|
||||||
|
|
||||||
function World:removeRoom(x, y)
|
function World:removeRoom(x, y)
|
||||||
|
|
@ -90,7 +165,13 @@ function World:removeRoom(x, y)
|
||||||
self.roomByName[room.name] = nil
|
self.roomByName[room.name] = nil
|
||||||
table.remove(self.rooms, i)
|
table.remove(self.rooms, i)
|
||||||
if self.lastRoom == room.name then self.lastRoom = nil end
|
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
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|
@ -111,7 +192,6 @@ end
|
||||||
function World:setLastRoom(name)
|
function World:setLastRoom(name)
|
||||||
if name and self.lastRoom ~= name then
|
if name and self.lastRoom ~= name then
|
||||||
self.lastRoom = name
|
self.lastRoom = name
|
||||||
return self:save()
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
@ -128,6 +208,600 @@ function World:getNeighbor(name, exit)
|
||||||
return self:roomAt(room.x + direction.x, room.y + direction.y)
|
return self:roomAt(room.x + direction.x, room.y + direction.y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- All placed levels share these sparse matrices. Rendering still uses the
|
||||||
|
-- selected Room, whose ordinary local matrices remain unchanged.
|
||||||
|
function World:rebuildCollision()
|
||||||
|
self.collidableMatrices = {red = {}, green = {}, blue = {}}
|
||||||
|
self.playerBlockerMatrices = {red = {}, green = {}, blue = {}}
|
||||||
|
self.entityRooms = {}
|
||||||
|
local function put(matrix, cell, entity)
|
||||||
|
matrix[cell.y] = matrix[cell.y] or {}
|
||||||
|
matrix[cell.y][cell.x] = entity
|
||||||
|
end
|
||||||
|
for name, room in pairs(self.levels) do
|
||||||
|
local location = self:roomLocation(name)
|
||||||
|
if location then
|
||||||
|
for color, entities in pairs(room.collideables) do
|
||||||
|
for _, entity in pairs(entities) do
|
||||||
|
self.entityRooms[entity] = room
|
||||||
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||||
|
put(self.collidableMatrices[color], {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}, entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- NPCs are local room state, but globally collidable: a movement lookup
|
||||||
|
-- must find them so its normal bump() path can open dialogue.
|
||||||
|
for _, npc in ipairs(room.npcs) do
|
||||||
|
self.entityRooms[npc] = room
|
||||||
|
for _, cell in ipairs(npc:getOccupiedCells()) do
|
||||||
|
local worldCell = {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}
|
||||||
|
for color in pairs(self.collidableMatrices) do put(self.collidableMatrices[color], worldCell, npc) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for color, entities in pairs(room.playerBlockers) do
|
||||||
|
for _, entity in pairs(entities) do
|
||||||
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||||
|
put(self.playerBlockerMatrices[color], {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}, entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:worldCell(room, cell)
|
||||||
|
local location = self:roomLocation(room.name)
|
||||||
|
return location and {x = (location.x - 1) * gridWidth + cell.x, y = (location.y - 1) * gridHeight + cell.y}
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:roomCell(cell)
|
||||||
|
local roomX = math.floor((cell.x - 1) / gridWidth) + 1
|
||||||
|
local roomY = math.floor((cell.y - 1) / gridHeight) + 1
|
||||||
|
local name = self:roomAt(roomX, roomY)
|
||||||
|
local room = name and self.levels[name]
|
||||||
|
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
|
||||||
|
local context = {}
|
||||||
|
function context:worldCell(cell) return world:worldCell(room, cell) end
|
||||||
|
function context:isInBounds(cell) return world:roomCell(cell) ~= nil end
|
||||||
|
function context:entityAt(cell)
|
||||||
|
local row = world.collidableMatrices[color][cell.y]
|
||||||
|
local entity = row and row[cell.x]
|
||||||
|
if not entity then return nil end
|
||||||
|
if proxies[entity] then return proxies[entity] end
|
||||||
|
local owner = world.entityRooms[entity]
|
||||||
|
local position = world:worldCell(owner, entity:getGridPos())
|
||||||
|
local proxy = {entity = entity, room = owner, color = color, x = position.x, y = position.y}
|
||||||
|
function proxy:canMove() return self.entity:canMove() end
|
||||||
|
function proxy:canPassOver() return self.entity:canPassOver() end
|
||||||
|
function proxy:isSticky() return self.entity:isSticky() end
|
||||||
|
function proxy:isSkewer() return self.entity:isSkewer() end
|
||||||
|
function proxy:isSpiky() return self.entity:isSpiky() end
|
||||||
|
function proxy:bump() return self.entity:bump() end
|
||||||
|
function proxy:getOccupiedCells(offset)
|
||||||
|
offset = offset or {x = 0, y = 0}
|
||||||
|
local cells = {}
|
||||||
|
for _, shapeCell in ipairs(self.entity:getCellShape()) do
|
||||||
|
table.insert(cells, {x = self.x + shapeCell.x - 1 + offset.x, y = self.y + shapeCell.y - 1 + offset.y})
|
||||||
|
end
|
||||||
|
return cells
|
||||||
|
end
|
||||||
|
proxies[entity] = proxy
|
||||||
|
return proxy
|
||||||
|
end
|
||||||
|
function context:playerBlockerAt(cell)
|
||||||
|
local row = world.playerBlockerMatrices[color][cell.y]
|
||||||
|
return row and row[cell.x]
|
||||||
|
end
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Player copies live in room files, but joining is a world-coordinate rule.
|
||||||
|
-- This also lets a copy rejoin immediately when the player crosses into its
|
||||||
|
-- room, rather than waiting for one more local move.
|
||||||
|
function World:playerJoinCheck(room, previousPositions, newPositions)
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
local deactivate, activate = {}, {}
|
||||||
|
if previousPositions and newPositions and #activeColors > 1 then
|
||||||
|
for _, color in ipairs(activeColors) do
|
||||||
|
if Entity.positionsAreEqual(previousPositions[color], newPositions[color]) then
|
||||||
|
table.insert(deactivate, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for color, player in pairs(room.player) do
|
||||||
|
if not room:colorIsActive(color) then
|
||||||
|
local playerCell = self:worldCell(room, player:getGridPos())
|
||||||
|
local overlaps = playerCell and #activeColors > 0
|
||||||
|
for _, activeColor in ipairs(activeColors) do
|
||||||
|
local activeCell = self:worldCell(room, room.player[activeColor]:getGridPos())
|
||||||
|
if not activeCell or activeCell.x ~= playerCell.x or activeCell.y ~= playerCell.y then overlaps = false; break end
|
||||||
|
end
|
||||||
|
if overlaps then table.insert(activate, color) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, color in ipairs(activate) do room:setActiveColor(color, true) end
|
||||||
|
for _, color in ipairs(deactivate) do room:setActiveColor(color, false) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:movePlayer(room, key)
|
||||||
|
local directions = {
|
||||||
|
up = {x = 0, y = -1, axis = "y"}, down = {x = 0, y = 1, axis = "y"},
|
||||||
|
left = {x = -1, y = 0, axis = "x"}, right = {x = 1, y = 0, axis = "x"},
|
||||||
|
}
|
||||||
|
local dir = directions[key]
|
||||||
|
if not dir then return end
|
||||||
|
local moved, previous, current = false, {}, {}
|
||||||
|
for _, color in ipairs(room:getActiveColors()) do
|
||||||
|
local player = room.player[color]
|
||||||
|
local pos = player:getGridPos()
|
||||||
|
previous[color] = pos
|
||||||
|
local context = self:movementContext(room, color)
|
||||||
|
local target = context:worldCell({x = pos.x + dir.x, y = pos.y + dir.y})
|
||||||
|
local targetEntity = context:entityAt(target)
|
||||||
|
if targetEntity then targetEntity:bump() end
|
||||||
|
local blocker = context:playerBlockerAt(target)
|
||||||
|
local canMove, pushing = true, {}
|
||||||
|
if context:isInBounds(target) and not (blocker and blocker:blocksPlayer()) then
|
||||||
|
canMove, pushing = Movement.findMovableGroup({target}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
elseif blocker then
|
||||||
|
canMove = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local pulling, pullingGroup = false, {}
|
||||||
|
local behind = context:worldCell({x = pos.x - dir.x, y = pos.y - dir.y})
|
||||||
|
if context:isInBounds(behind) then
|
||||||
|
pulling, pullingGroup = Movement.findMovableGroup({behind}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
end
|
||||||
|
if canMove then
|
||||||
|
local group = {}
|
||||||
|
for entity in pairs(pushing) do group[entity] = entity end
|
||||||
|
if pulling then for entity in pairs(pullingGroup) do group[entity] = entity end end
|
||||||
|
self:moveGroup(group, dir)
|
||||||
|
player:move(dir.axis, dir[dir.axis])
|
||||||
|
moved = true
|
||||||
|
end
|
||||||
|
current[color] = player:getGridPos()
|
||||||
|
end
|
||||||
|
if moved then
|
||||||
|
self:markDirty(room)
|
||||||
|
self:playerJoinCheck(room, previous, current)
|
||||||
|
self:tick(room)
|
||||||
|
self:flushRuntime()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:switchPlayerColor(room, color)
|
||||||
|
local player = room.player[color]
|
||||||
|
if not player then return false end
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
local canSwitch = #activeColors ~= 1 or activeColors[1] ~= color
|
||||||
|
if canSwitch then
|
||||||
|
for _, possibleColor in ipairs(room:getPossibleColors()) do
|
||||||
|
room:setActiveColor(possibleColor, possibleColor == color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
player:playSwitchSound(canSwitch)
|
||||||
|
if canSwitch then self:markDirty(room); self:flushRuntime() end
|
||||||
|
return canSwitch
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:tick(room)
|
||||||
|
room:tickLocal()
|
||||||
|
self:rebuildCollision()
|
||||||
|
self:resolveCircuits()
|
||||||
|
self:rebuildCollision()
|
||||||
|
self:nextRoomCheck(room)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:nextRoomCheck(room)
|
||||||
|
for _, color in ipairs(room:getActiveColors()) do
|
||||||
|
local player = room.player[color]
|
||||||
|
if player and not room:isInBounds(player:getGridPos()) then
|
||||||
|
self:nextRoom(room, player:getGridPos())
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:nextRoom(room, pos)
|
||||||
|
local exit, entry = nil, {x = pos.x, y = pos.y}
|
||||||
|
if pos.x > gridWidth then exit, entry.x = "right", 1
|
||||||
|
elseif pos.x < 1 then exit, entry.x = "left", gridWidth
|
||||||
|
elseif pos.y > gridHeight then exit, entry.y = "bottom", 1
|
||||||
|
elseif pos.y < 1 then exit, entry.y = "top", gridHeight end
|
||||||
|
local nextName = self:getNeighbor(room.name, exit) or room.name
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
self:markDirty(room)
|
||||||
|
local destination = self.levels[nextName]
|
||||||
|
for _, color in ipairs(activeColors) do
|
||||||
|
local player = room.player[color]
|
||||||
|
-- Active copies travel with the player. Inactive copies are deliberately
|
||||||
|
-- left in their room so World:playerJoinCheck can merge them later.
|
||||||
|
room.player[color] = nil
|
||||||
|
for i = #room.colorsPlayerHas, 1, -1 do
|
||||||
|
if room.colorsPlayerHas[i] == color then table.remove(room.colorsPlayerHas, i) end
|
||||||
|
end
|
||||||
|
if destination.player[color] then
|
||||||
|
for i = #destination.colorsPlayerHas, 1, -1 do
|
||||||
|
if destination.colorsPlayerHas[i] == color then table.remove(destination.colorsPlayerHas, i) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
destination.player[color] = player
|
||||||
|
table.insert(destination.colorsPlayerHas, color)
|
||||||
|
end
|
||||||
|
room.activeColors = {red = false, green = false, blue = false}
|
||||||
|
currentRoom = destination
|
||||||
|
self:markDirty(currentRoom)
|
||||||
|
for _, color in ipairs(activeColors) do
|
||||||
|
currentRoom.player[color].x, currentRoom.player[color].y = entry.x, entry.y
|
||||||
|
currentRoom:setActiveColor(color, true)
|
||||||
|
end
|
||||||
|
self:playerJoinCheck(currentRoom)
|
||||||
|
self.lastRoom = currentRoom.name
|
||||||
|
self:resolveCircuits()
|
||||||
|
self:rebuildCollision()
|
||||||
|
setEditorRoomName(currentRoom.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:moveGroup(group, direction)
|
||||||
|
local changed = {}
|
||||||
|
for proxy in pairs(group) do
|
||||||
|
changed[proxy.room] = true
|
||||||
|
for _, cell in ipairs(proxy:getOccupiedCells()) do
|
||||||
|
local room = self:roomCell(cell)
|
||||||
|
if room then changed[room] = true end
|
||||||
|
end
|
||||||
|
for _, cell in ipairs(proxy:getOccupiedCells(direction)) do
|
||||||
|
local room = self:roomCell(cell)
|
||||||
|
if room then changed[room] = true end
|
||||||
|
end
|
||||||
|
local destination, localCell = self:roomCell({x = proxy.x + direction.x, y = proxy.y + direction.y})
|
||||||
|
if destination then
|
||||||
|
if destination ~= proxy.room then
|
||||||
|
for i = #proxy.room.collideables[proxy.color], 1, -1 do
|
||||||
|
if proxy.room.collideables[proxy.color][i] == proxy.entity then table.remove(proxy.room.collideables[proxy.color], i); break end
|
||||||
|
end
|
||||||
|
table.insert(destination.collideables[proxy.color], proxy.entity)
|
||||||
|
self.entityRooms[proxy.entity] = destination
|
||||||
|
changed[proxy.room], changed[destination] = true, true
|
||||||
|
end
|
||||||
|
proxy.entity.x, proxy.entity.y = localCell.x, localCell.y
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
function World:drawOverlaps(room, color)
|
||||||
|
local location = self:roomLocation(room.name)
|
||||||
|
if not location then return end
|
||||||
|
local drawn = {}
|
||||||
|
for y = 1, gridHeight do
|
||||||
|
local row = self.collidableMatrices[color][(location.y - 1) * gridHeight + y]
|
||||||
|
if row then
|
||||||
|
for x = 1, gridWidth do
|
||||||
|
local entity = row[(location.x - 1) * gridWidth + x]
|
||||||
|
local owner = entity and self.entityRooms[entity]
|
||||||
|
if entity and owner ~= room and not drawn[entity] then
|
||||||
|
drawn[entity] = true
|
||||||
|
local ownerLocation = self:roomLocation(owner.name)
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.translate((ownerLocation.x - location.x) * width, (ownerLocation.y - location.y) * height)
|
||||||
|
entity:draw(color)
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function World:getCellFromMousePos(mouseX, mouseY)
|
function World:getCellFromMousePos(mouseX, mouseY)
|
||||||
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
|
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
|
||||||
end
|
end
|
||||||
|
|
@ -149,8 +823,8 @@ function World:invalidatePreview(name)
|
||||||
self.previewCache[name] = nil
|
self.previewCache[name] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function World:drawRoomPreview(name, x, y, size)
|
function World:drawRoomPreview(name, x, y, size, roomData)
|
||||||
local room = self:getRoomData(name)
|
local room = roomData or self:getRoomData(name)
|
||||||
if not room then return end
|
if not room then return end
|
||||||
local scale = size / (gridWidth * 16)
|
local scale = size / (gridWidth * 16)
|
||||||
love.graphics.setScissor(x, y, size, size)
|
love.graphics.setScissor(x, y, size, size)
|
||||||
|
|
@ -189,6 +863,77 @@ function World:drawRoomPreview(name, x, y, size)
|
||||||
love.graphics.setScissor()
|
love.graphics.setScissor()
|
||||||
end
|
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()
|
function World:drawHoveredRoomName()
|
||||||
local mouseX, mouseY = love.mouse.getPosition()
|
local mouseX, mouseY = love.mouse.getPosition()
|
||||||
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
|
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue