diff --git a/level_editor/editor.lua b/level_editor/editor.lua index 55987ef..ce1e8af 100644 --- a/level_editor/editor.lua +++ b/level_editor/editor.lua @@ -44,11 +44,6 @@ local worldSearchText = "" local worldSearchBox = { x = 10, y = 34, w = 0, h = 28 } local cloneSourceRoom = nil local roomBrush = nil -local wireBrush = nil -local selectedCircuitId = nil -local wireCameraX, wireCameraY = nil, nil -local wireDirtyRooms = {} -local wireOriginalWires = nil local boxPaletteOpen = false local boxPaletteScroll = { scroll = 0 } local boxPaletteSections = {} @@ -278,8 +273,6 @@ local function layoutRoomControls(c, s) end b.scale = math.min(target / spriteW, target / spriteH) b.w, b.h = spriteW * b.scale, spriteH * b.scale - else - b.w, b.h = 72, target end return b end @@ -303,8 +296,8 @@ local function layoutRoomControls(c, s) end placeRow({ "white", "red", "green", "blue" }, 22) - placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box" }, 22) - placeRow({ "save", "clearSaves", "world", "wire" }, 28) + placeRow({ "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box" }, 22) + placeRow({ "save", "clearSaves", "world" }, 28) end -- Build the full room-view layout (sections + a scroll list + a back/search @@ -499,74 +492,10 @@ local function saveRoomFromEditor() gameWorld:invalidatePreview(name) if editorView == "world" then rebuildWorldRoomButtons() end currentRoom.name = name - gameWorld:setLevel(currentRoom) editorRoomName = name roomNameField:editText(name) activeField = nil editorStatus = "Saved source level '" .. name .. "'" .. (savePlayerPositions and " with player positions." or " without player positions.") - - -- A circuit stroke can touch neighbouring rooms in one gesture. Save those - -- authored fragments together so a cross-room route cannot be half-written. - for room in pairs(wireDirtyRooms) do - if room ~= currentRoom and room.name then - room:saveWiresToSource() - gameWorld:invalidatePreview(room.name) - clearRuntimeSaveData(room.name) - end - end - wireDirtyRooms = {} -end - -local function copyWires(wires) - local copy = {} - for y, row in pairs(wires or {}) do - copy[y] = {} - for x, tracks in pairs(row) do - copy[y][x] = {} - for circuitId, mask in pairs(tracks) do copy[y][x][circuitId] = mask end - end - end - return copy -end - -local function resumeWireRoom(status) - local name = wireCameraX and wireCameraY and gameWorld:roomAt(wireCameraX, wireCameraY) - local room = name and gameWorld.levels[name] - if not room then - room = currentRoom - status = status .. " The centered world cell is empty, so the previous room remains open." - end - currentRoom = room - gameWorld:setLevel(currentRoom) - gameWorld:setLastRoom(currentRoom.name) - setEditorRoomName(currentRoom.name) - wireOriginalWires = nil - wireDirtyRooms = {} - assetsView() - editorStatus = status -end - -local function saveWireRooms() - local saved = 0 - for room in pairs(wireDirtyRooms) do - local success = room == currentRoom and room:saveToSource(room.name) or room:saveWiresToSource() - if room.name and success then - saved = saved + 1 - gameWorld:invalidatePreview(room.name) - clearRuntimeSaveData(room.name) - end - end - suppressRuntimeAutosave = true - local status = saved > 0 and ("Saved wire fragments in " .. saved .. " room(s).") or "No wire changes to save." - resumeWireRoom(status) -end - - -local function discardWireRooms() - for room, wires in pairs(wireOriginalWires or {}) do room.wires = copyWires(wires) end - gameWorld:resolveCircuits() - gameWorld:rebuildCollision() - resumeWireRoom("Discarded wire changes.") end local function clearRuntimeSaves() @@ -591,7 +520,6 @@ local function clearRuntimeSaves() clearSaveConfirmationScope = nil if roomName then currentRoom = Room:new{ name = roomName } - gameWorld:setLevel(currentRoom) setEditorRoomName(currentRoom.name) editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'" else @@ -701,7 +629,7 @@ function _initLevelEditor() }, color) end - for _, asset in ipairs({ "player", "static_wall", "switch", "door", "wired_switch", "wired_door", "box" }) do + for _, asset in ipairs({ "player", "static_wall", "switch", "door", "box" }) do local properties = gameAssets[asset] if properties then local sprites = {} @@ -720,16 +648,6 @@ function _initLevelEditor() end end - _addUIButton(createUIElement{ - name = "wire", - text = "WIRES", - onClic = { left = function() - if editorView == "wire" then assetsView() else wireView() end - end }, - x = 0, - y = 0, - }, "wire") - roomNameField = createUIElement{ name = "roomName", text = editorRoomName, @@ -919,31 +837,11 @@ end function assetsView() editingAssetButtons = {} editorView = "room" - wireBrush = nil labels = {} end -function wireView() - local location = currentRoom and gameWorld:roomLocation(currentRoom.name) - if not location then - editorStatus = "Place this room in the world before drawing cross-room wires." - return - end - editorView = "wire" - activeField = nil - roomBrush = nil - wireCameraX, wireCameraY = location.x, location.y - wireOriginalWires = {} - for _, room in pairs(gameWorld.levels or {}) do wireOriginalWires[room] = copyWires(room.wires) end - wireDirtyRooms = {} - editorStatus = selectedCircuitId - and "Circuit selected. Drag to draw; right-drag erases; arrow keys pan." - or "Click a wired door to select its circuit, then drag to its switches." -end - function worldView() editorView = "world" - wireBrush = nil activeField = nil selectedFolder = nil selectedRoom = nil @@ -952,118 +850,6 @@ function worldView() rebuildWorldRoomButtons() end -local function wireControls() - local pad, gap, h = 12, 8, 40 - local w = (sb.w - pad * 2 - gap) / 2 - return { - save = {x = pad, y = sb.h - pad - h, w = w, h = h, text = "SAVE"}, - discard = {x = pad + w + gap, y = sb.h - pad - h, w = w, h = h, text = "DISCARD"}, - } -end - -local function rememberWireRooms(rooms) - for _, room in ipairs(rooms or {}) do wireDirtyRooms[room] = true end -end - -local function wireCellAt(x, y) - if x < 0 or x >= width or y < 0 or y >= height then return nil end - if not wireCameraX or not wireCameraY then return nil end - local cell = gameWorld:wireEditorCellAt(x, y, wireCameraX, wireCameraY) - local room = gameWorld:roomCell(cell) - return room and cell or nil -end - -local function paintWireStep(from, to, button) - if button == 1 then - local rooms, err = gameWorld:addWireConnection(from, to, selectedCircuitId) - rememberWireRooms(rooms) - if err then editorStatus = err end - else - rememberWireRooms(gameWorld:removeWireAt(to, selectedCircuitId)) - end -end - -local function paintWireLine(from, to, button) - local current = {x = from.x, y = from.y} - local function walk(axis, target) - local step = current[axis] < target and 1 or -1 - while current[axis] ~= target do - local nextCell = {x = current.x, y = current.y} - nextCell[axis] = nextCell[axis] + step - local room = gameWorld:roomCell(nextCell) - if not room then return false end - paintWireStep(current, nextCell, button) - current = nextCell - end - return true - end - -- Manhattan interpolation keeps every authored segment orthogonal even if - -- the mouse skips diagonally across several cells between move events. - if not walk("x", to.x) then return current end - walk("y", to.y) - return current -end - -local function beginWireBrush(x, y, button) - if button ~= 1 and button ~= 2 then return end - local cell = wireCellAt(x, y) - if not cell then return end - if button == 1 then - local door = gameWorld:wiredDoorAt(cell) - if door then - -- Each stroke leaving a door is its own run and may terminate at one - -- logical switch. Multiple runs can meet at the door, whose state is the - -- AND of those independently coloured switch terminals. - selectedCircuitId = gameWorld:newCircuitId() - editorStatus = "Started " .. selectedCircuitId .. ". End this run on one switch." - else - -- A wire is a selectable terminal even after its door is deleted. - -- Clicking empty space keeps the current circuit so a new branch can - -- still begin away from the existing route. - local circuitAtCell = gameWorld:circuitAt(cell) - if circuitAtCell then - selectedCircuitId = circuitAtCell - editorStatus = "Selected orphaned circuit " .. selectedCircuitId .. "." - end - end - if not selectedCircuitId then - editorStatus = "Start by clicking a wired door." - return - end - rememberWireRooms(gameWorld:ensureWireAt(cell, selectedCircuitId)) - else - -- Prefer the selected circuit when several routes overlap here; otherwise - -- adopt the top route. Repeated right-clicks can therefore remove every - -- orphan at an overlapping cell without needing a door. - local circuitAtCell = gameWorld:circuitAt(cell, selectedCircuitId) - if not circuitAtCell then - editorStatus = "There is no wire at this cell to erase." - return - end - selectedCircuitId = circuitAtCell - rememberWireRooms(gameWorld:removeWireAt(cell, selectedCircuitId)) - editorStatus = "Erasing " .. selectedCircuitId .. "." - end - wireBrush = {button = button, lastCell = cell} -end - -function editorWireKeypressed(key) - if editorView ~= "wire" then return false end - local directions = { - left = {-1, 0}, right = {1, 0}, up = {0, -1}, down = {0, 1}, - } - local direction = directions[key] - if not direction then return false end - wireCameraX = constrain(wireCameraX + direction[1], 1, gameWorld.width) - wireCameraY = constrain(wireCameraY + direction[2], 1, gameWorld.height) - wireBrush = nil - return true -end - -function wireEditorCamera() - return wireCameraX, wireCameraY, selectedCircuitId -end - local function newWorldRoomName(cellX, cellY) local base = string.format("room_%02d_%02d", cellX, cellY) local name, suffix = base, 2 @@ -1084,23 +870,6 @@ local function cloneWorldRoom(sourceName, cloneName) editorStatus = "A level named '" .. cloneName .. "' already exists." return false end - -- A clone is a new puzzle slice, so remap every circuit identity found in - -- it. Keeping the originals would let the clone silently join the source's - -- cross-room wires when both are placed in the same world. - local ok, saveTable = pcall(TSerial.unpack, source) - if ok and type(saveTable) == "table" then - local remapped = {} - local function remap(circuitId) - if not circuitId then return nil end - if not remapped[circuitId] then remapped[circuitId] = gameWorld:newCircuitId() end - return remapped[circuitId] - end - for _, object in ipairs(saveTable.objects or {}) do - if object.circuit then object.circuit = remap(object.circuit) end - end - for _, wire in ipairs(saveTable.wires or {}) do wire.circuit = remap(wire.circuit) end - source = TSerial.pack(saveTable, nil, true) - end if not writeToSource("rooms/" .. cloneName .. ".sav", source) then editorStatus = "Could not create clone '" .. cloneName .. "'." return false @@ -1136,7 +905,6 @@ local function openWorldRoom(cellX, cellY) currentRoom = Room:new{ name = name } editorStatus = "Editing '" .. name .. ".'" end - gameWorld:setLevel(currentRoom) gameWorld:setLastRoom(currentRoom.name) setEditorRoomName(currentRoom.name) assetsView() @@ -1190,17 +958,13 @@ function editorMouseHandler(x, y, button) end -- control cluster (only the buttons that live there) - for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire" }) do + for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) do local b = uiButtons[name] if b and b:clicIsInBox(x, y) then b:onClic(button) return end end - elseif editorView == "wire" then - local controls = wireControls() - if buttonContains(controls.save, x, y) then saveWireRooms(); return end - if buttonContains(controls.discard, x, y) then discardWireRooms(); return end elseif editorView == "asset" then for _, uiElement in pairs(editingAssetButtons) do if uiElement:clicIsInBox(x, y) then @@ -1238,8 +1002,6 @@ function editorMouseHandler(x, y, button) -- click landed on the grid (game area): place the selected asset if editorView == "room" then beginRoomBrush(x, y, button) - elseif editorView == "wire" then - beginWireBrush(x, y, button) elseif editorView == "world" then local cellX, cellY = gameWorld:getCellFromMousePos(x, y) if not gameWorld:isInBounds(cellX, cellY) then return end @@ -1262,15 +1024,6 @@ function editorMouseHandler(x, y, button) end function editorMouseMoved(x, y) - if wireBrush then - if editorView ~= "wire" then wireBrush = nil; return end - local cell = wireCellAt(x, y) - if not cell then wireBrush = nil; return end - if cell.x ~= wireBrush.lastCell.x or cell.y ~= wireBrush.lastCell.y then - wireBrush.lastCell = paintWireLine(wireBrush.lastCell, cell, wireBrush.button) - end - return - end if not roomBrush then return end if editorView ~= "room" then roomBrush = nil @@ -1292,7 +1045,6 @@ end function editorMouseReleased(_, _, button) if roomBrush and roomBrush.button == button then roomBrush = nil end - if wireBrush and wireBrush.button == button then wireBrush = nil end end function editorTextHandler(text) @@ -1342,7 +1094,6 @@ function editorCompleteResponse() end function editorCancelResponse() - if editorView == "wire" then discardWireRooms(); return true end if editorView ~= "world" or activeField ~= "cloneName" then return false end activeField = nil cloneSourceRoom = nil @@ -1402,15 +1153,11 @@ function editorDraw() -- control cluster love.graphics.setColor(gColor.white:set()) love.graphics.printf("LEVEL NAME", s.controls.x, s.labelY, s.controls.w, "center") - for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_wired_switch", "quick_wired_door", "quick_box", "save", "clearSaves", "world", "wire" }) do + for _, name in ipairs({ "roomName", "white", "red", "green", "blue", "quick_player", "quick_static_wall", "quick_switch", "quick_door", "quick_box", "save", "clearSaves", "world" }) do local button = uiButtons[name] - if button then - button:draw() - local asset = name:match("^quick_(.+)$") - if asset and asset:match("^wired_") then - love.graphics.setColor(1, 1, 1, 1) - love.graphics.print("W", button.x + button.w - 7, button.y - 4) - end + if button then + button:draw() + local asset = name:match("^quick_(.+)$") if asset and selectedAsset == asset then love.graphics.setColor(1, 0.85, 0.2, 1) love.graphics.setLineWidth(2) @@ -1426,32 +1173,6 @@ function editorDraw() love.graphics.printf(editorStatus, s.status.x, s.status.y + 4, s.status.w, "center") Layout.unclip() end - elseif editorView == "wire" then - local circuitColor = selectedCircuitId and require("wire").color(selectedCircuitId) - love.graphics.setColor(gColor.white:set()) - love.graphics.printf("CIRCUIT WIRING", 10, 16, sb.w - 20, "center") - if selectedCircuitId then - love.graphics.setColor(circuitColor[1], circuitColor[2], circuitColor[3], 1) - love.graphics.circle("fill", 28, 65, 9) - love.graphics.setColor(gColor.white:set()) - love.graphics.printf(selectedCircuitId, 46, 54, sb.w - 58, "left") - else - love.graphics.printf("No circuit selected", 10, 54, sb.w - 20, "center") - end - love.graphics.printf( - "Click a door to start a new run, or a wire to edit that run. Each run ends at one switch; start another run from the door for another switch.\n\nLeft-drag draws. Right-drag erases. Independent wires may share a cell without connecting.\n\nArrow keys pan by one room.", - 24, 100, sb.w - 48, "left" - ) - if editorStatus then - love.graphics.setColor(gColor.white:set()) - love.graphics.printf(editorStatus, 24, sb.h * 0.55, sb.w - 48, "center") - end - local controls = wireControls() - for _, control in pairs(controls) do - love.graphics.setColor(gColor.white:set()) - love.graphics.rectangle("line", control.x, control.y, control.w, control.h) - love.graphics.printf(control.text, control.x, control.y + 11, control.w, "center") - end elseif editorView == "asset" then for _, button in pairs(editingAssetButtons) do button:draw() @@ -1465,7 +1186,7 @@ function editorDraw() end -- The room view draws its status inside its own strip (see the room branch); -- the other views keep the simple centered line. - if editorStatus and editorView ~= "room" and editorView ~= "wire" then + if editorStatus and editorView ~= "room" then love.graphics.setColor(gColor.white:set()) love.graphics.printf(editorStatus, 10, sb.h * (6 / 10), sb.w - 20, "center") end diff --git a/level_editor/object_attributes.lua b/level_editor/object_attributes.lua index e63cacc..9cab6c9 100644 --- a/level_editor/object_attributes.lua +++ b/level_editor/object_attributes.lua @@ -59,23 +59,5 @@ function getAllAssets() end end - -- Wired devices intentionally reuse the familiar switch/door artwork. Their - -- white terminal marker is drawn by the entity class, while these synthetic - -- asset records make them available to the ordinary palette and brush. - local function synthetic(name, source, behavior) - local original = globalAssetProperties[source] - if not original then return end - globalAssetProperties[name] = { - directory = original.directory, - sprites = original.sprites, - cells = original.cells, - class = behavior, - preload = false, - synthetic = true, - } - end - synthetic("wired_switch", "switch", "wired_switch") - synthetic("wired_door", "door", "wired_door") - return BoxVariants.addTo(globalAssetProperties) end diff --git a/main.lua b/main.lua index a13d9ed..2d0b76a 100644 --- a/main.lua +++ b/main.lua @@ -35,9 +35,6 @@ function love.load() _initAccessibility() gameWorld = World:new() _initLevelEditor() - -- getAllAssets runs in _initLevelEditor; deserialize the world only after - -- its asset metadata exists, otherwise every non-player object is skipped. - gameWorld:loadLevels() _initCanvases() @@ -46,16 +43,10 @@ function love.load() -- 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 -- through their own (source-only) load path, so this only affects resume. - local initialRoom = gameWorld.lastRoom or "start" - -- Preserve the old resume behavior for the active room while the rest of the - -- 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 + currentRoom = Room:new{ + name = gameWorld.lastRoom or "start", + runtime = true + } gameWorld:setLastRoom(currentRoom.name) setEditorRoomName(currentRoom.name) @@ -68,21 +59,17 @@ function love.quit() -- An explicit editor save/clear removes the runtime copy intentionally; -- don't immediately recreate it while shutting down. if currentRoom and not suppressRuntimeAutosave then - if gameWorld then - gameWorld:markDirty(currentRoom) - gameWorld:flushRuntime() - else - currentRoom:save() - end + currentRoom:save() + if gameWorld then gameWorld:setLastRoom(currentRoom.name) end end end function love.update(dt) - if editorView == "world" or editorView == "wire" then + if editorView == "world" then Input:stopMoveRepeat() else local repeatedMove = Input:handler(dt) - if repeatedMove and not Dialogue.isOpen() then gameWorld:movePlayer(currentRoom, repeatedMove) end + if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end end currentRoom:update(dt) @@ -96,8 +83,6 @@ function love.draw() love.graphics.setBlendMode("screen") if editorView == "world" then gameWorld:draw() - elseif editorView == "wire" then - gameWorld:drawWireEditor(wireEditorCamera()) else currentRoom:draw() end @@ -177,14 +162,12 @@ function love.keypressed(key) return end - if editorView == "wire" and editorWireKeypressed(key) then return end - - if editorView ~= "world" and editorView ~= "wire" and Input.isPlayerMove(key) then + if editorView ~= "world" and Input.isPlayerMove(key) then Input:beginMove(key) - gameWorld:movePlayer(currentRoom, key) - elseif editorView ~= "world" and editorView ~= "wire" and Input.isPlayerSwitch(key) then + currentRoom:movePlayer(key) + elseif editorView ~= "world" and Input.isPlayerSwitch(key) then local c = Input.playerColor(key) - gameWorld:switchPlayerColor(currentRoom, c) + currentRoom:switchPlayerColor(c) end -- if key == "w" then diff --git a/room.lua b/room.lua index f38e8cc..d354618 100644 --- a/room.lua +++ b/room.lua @@ -8,8 +8,6 @@ require "art" require "assorted" require "switch" require "door" -require "wired_switch" -require "wired_door" require "dialogue_manager" require "generic_moveable" require "generic_sticky" @@ -19,15 +17,11 @@ require "generic_unmoveable" require "generic_player_blocker" local Movement = require "movement" -local Wire = require "wire" function Room:initialize(t) self.player = {} self.switches = { red = {}, green = {}, blue = {} } self.doors = {} - self.wiredSwitches = {} - self.wiredDoors = {} - self.wires = {} self.collideables = { red = {}, green = {}, blue = {} } self.playerBlockers = { red = {}, green = {}, blue = {} } self.npcs = {} @@ -131,7 +125,6 @@ function Room:update(dt) end function Room:draw() - self:drawWiresAt(0, 0, width / gridWidth, nil, false) -- draw only the channels the player has. Each object just exposes sprites per -- channel via Entity:draw(channel); a door registered on all three channels @@ -139,120 +132,12 @@ function Room:draw() for _, channel in ipairs(self.colorsPlayerHas) do if self.player[channel] then self.player[channel]: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 _, switch in pairs(self.switches[channel]) do switch:draw(channel) end - for _, switch in ipairs(self.wiredSwitches) do switch:draw(channel) end for _, npc in ipairs(self.npcs) do npc:draw(channel) end end end -function Room:wireTracksAt(cell, create) - local row = self.wires[cell.y] - if not row and create then row = {}; self.wires[cell.y] = row end - local tracks = row and row[cell.x] - if not tracks and create then tracks = {}; row[cell.x] = tracks end - return tracks -end - -function Room:setWireMask(cell, circuitId, mask) - if not self:isInBounds(cell) then return false end - local tracks = self:wireTracksAt(cell, true) - tracks[circuitId] = mask or 0 - return true -end - -function Room:wireCircuitAt(cell) - local tracks = self:wireTracksAt(cell) - local ids = {} - for circuitId in pairs(tracks or {}) do table.insert(ids, circuitId) end - table.sort(ids) - return ids[1] -end - -function Room:removeWire(cell, circuitId) - local tracks = self:wireTracksAt(cell) - if not tracks or tracks[circuitId] == nil then return false end - tracks[circuitId] = nil - if not next(tracks) then self.wires[cell.y][cell.x] = nil end - if not next(self.wires[cell.y]) then self.wires[cell.y] = nil end - return true -end - -function Room:drawWiresAt(originX, originY, cellSize, selectedCircuit, editorColors) - local ids = {} - for _, row in pairs(self.wires) do - for _, tracks in pairs(row) do - for circuitId in pairs(tracks) do ids[circuitId] = true end - end - end - local ordered = {} - for circuitId in pairs(ids) do table.insert(ordered, circuitId) end - table.sort(ordered) - - love.graphics.setBlendMode("alpha") - local visibleColors = {red = false, green = false, blue = false} - for _, color in ipairs(self.colorsPlayerHas or {}) do visibleColors[color] = true end - local editorIsOn = editorView ~= nil and editorView ~= "off" - for _, circuitId in ipairs(ordered) do - local selected = not selectedCircuit or selectedCircuit == circuitId - for y, row in pairs(self.wires) do - for x, tracks in pairs(row) do - local mask = tracks[circuitId] - if mask ~= nil then - local localCell = {x = x, y = y} - local switchColors = gameWorld and gameWorld.wireSwitchColorsAt and - gameWorld:wireSwitchColorsAt(self, localCell, circuitId) - local drawColors = {} - if editorColors then - -- Connected circuits retain their ID colour in the wiring tool; - -- switchless fragments are neutral so orphans are obvious. - table.insert(drawColors, switchColors and Wire.color(circuitId) or {0.52, 0.52, 0.58}) - elseif switchColors then - -- Draw literal RGB passes taken from the switch entities at this - -- run's sole terminal. The room's player channels decide which - -- passes exist, exactly as they do for every other entity. - for _, channel in ipairs({"red", "green", "blue"}) do - if switchColors[channel] and visibleColors[channel] then - local channelColor = gColor[channel] - table.insert(drawColors, {channelColor.r, channelColor.g, channelColor.b}) - end - end - elseif editorIsOn then - table.insert(drawColors, {0.52, 0.52, 0.58}) - end - - if #drawColors > 0 then - local cx = originX + (x - 0.5) * cellSize - local cy = originY + (y - 0.5) * cellSize - local lines = {} - for _, direction in ipairs(Wire.directions) do - if Wire.has(mask, direction.bit) then - table.insert(lines, {cx, cy, cx + direction.x * cellSize / 2, cy + direction.y * cellSize / 2}) - end - end - if #lines == 0 then table.insert(lines, {cx, cy, cx, cy}) end - -- The dark under-stroke makes later tracks cut a readable gap in - -- earlier tracks when two independent circuits cross. - love.graphics.setColor(0.03, 0.03, 0.05, selected and 1 or 0.7) - love.graphics.setLineWidth(math.max(1.5, cellSize * 0.10)) - for _, line in ipairs(lines) do love.graphics.line(line) end - local wireBlend = (editorColors or not switchColors) and "alpha" or "screen" - love.graphics.setBlendMode(wireBlend) - love.graphics.setLineWidth(math.max(1, cellSize * 0.045)) - for _, color in ipairs(drawColors) do - love.graphics.setColor(color[1], color[2], color[3], selected and 1 or 0.32) - for _, line in ipairs(lines) do love.graphics.line(line) end - love.graphics.circle("fill", cx, cy, math.max(0.75, cellSize * 0.0275)) - end - end - end - end - end - end - love.graphics.setBlendMode("screen") -end - function Room:createCollideablesMatrix() for color, entities in pairs(self.collideables) do self.collidableMatrices[color] = self:createEntityMatrix(entities) @@ -296,7 +181,7 @@ function Room:createEntityMatrix(entities) for _, entity in pairs(entities) do for _, occupiedCells in pairs(entity:getOccupiedCells()) do --we store a table reference so we can call a method on it during a chec_ - if self:isInBounds(occupiedCells) then matrix[occupiedCells.y][occupiedCells.x] = entity end + matrix[occupiedCells.y][occupiedCells.x] = entity end end -- printMatrix(matrix) @@ -308,9 +193,7 @@ function Room:getCellInMatrix(matrix, cell) return matrix[cell.y][cell.x] end --- 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) +function Room:movePlayer(key) local moved = false local previousPositions = {} --these are so we can perform the overlap chec_ @@ -341,36 +224,23 @@ function Room:movePlayerLocal(key) x = pos.x + dir.x, y = pos.y + dir.y } -- 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 - if useWorld then - initialTargetEntity = context:entityAt(worldTarget) - elseif self:isInBounds(initialTargetCell) then + if self:isInBounds(initialTargetCell) then initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell) end if initialTargetEntity then initialTargetEntity:bump() end local playerBlocker - if useWorld then - playerBlocker = context:playerBlockerAt(worldTarget) - elseif self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then + if self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell) end - if useWorld and not context:isInBounds(worldTarget) then - move = true - elseif not useWorld and not self:isInBounds(initialTargetCell) then + if not self:isInBounds(initialTargetCell) then -- let's us move out of bounds to Change Rooms without pulling a bloc_ move = true elseif playerBlocker and playerBlocker:blocksPlayer() then 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 move, entitiesToPush = Movement.findMovableGroup( {initialTargetCell}, dir, @@ -383,11 +253,7 @@ function Room:movePlayerLocal(key) local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y} local pull = false - 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 + if self:isInBounds(initialTargetCell) then pull, entitiesToPull = Movement.findMovableGroup( {initialTargetCell}, dir, function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end, @@ -396,15 +262,10 @@ function Room:movePlayerLocal(key) end if move then - if useWorld then - 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 + for _, entity in pairs(entitiesToPush) do + entity:move(axis, dir[axis]) end - if pull == true and not useWorld then + if pull == true then for _, entity in pairs(entitiesToPull) do -- A connected component can wrap around the player and be found by -- both searches. Every entity still moves exactly one grid cell. @@ -420,20 +281,12 @@ function Room:movePlayerLocal(key) end if moved then - if useWorld then gameWorld:playerJoinCheck(self, previousPositions, newPositions) - else self:playerJoinCheck(previousPositions, newPositions) end + self:playerJoinCheck(previousPositions, newPositions) self:tick() end end -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) +function Room:switchPlayerColor(color) -- 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 -- to (or use as the sound source). @@ -451,13 +304,6 @@ function Room:switchPlayerColorLocal(color) return canSwitch 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) self.activeColors[color] = bool end @@ -515,7 +361,7 @@ function Room:onlyColorIsActive(color) end end -function Room:tickLocal() +function Room:tick() for color, entityArray in pairs(self.collideables) do for _, entity in pairs(entityArray) do entity:tick() @@ -525,11 +371,6 @@ function Room:tickLocal() self:createPlayerBlockerMatrix() self:createSwitchMatrix() 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() end @@ -604,14 +445,7 @@ function Room:switchCheck() for color, switchArray in pairs(self.switches) do local onSwitches = {} for _, switch in pairs(switchArray) do - 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 targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos()) local player = self.player[color] -- Players are not stored in collidableMatrices, but a colour copy -- standing on its matching switch presses it just like a block does. @@ -652,8 +486,7 @@ end -- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/ -- "moveable"/"sticky"/"skewer"/"spiky"/"player_blocker"); anything without a .meta defaults to immoveable. npc is the one -- keyword handled directly since it has no art. -function Room:createEntity(class, color, x, y, data) - data = data or {} +function Room:createEntity(class, color, x, y) if class == "npc" then local entity = Sign:new(x, y) self:registerNpc(entity) @@ -669,31 +502,7 @@ function Room:createEntity(class, color, x, y, data) local behavior = props and props.class or "immoveable" local entity - 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 + if behavior == "door" then -- Old migrated rooms stored a colourless shared door. Expand that legacy -- record into ordinary colour-layer door objects on load. if not color then @@ -761,15 +570,6 @@ function Room:registerSwitch(entityPointer, color) table.insert(self.switches[color], entityPointer) end -function Room:registerWiredDoor(entityPointer, color) - table.insert(self.wiredDoors, entityPointer) - self:registerCollidable(entityPointer, color) -end - -function Room:registerWiredSwitch(entityPointer) - table.insert(self.wiredSwitches, entityPointer) -end - function Room:registerPlayer(entityPointer, color) table.insert(self.colorsPlayerHas, color) self.player[color] = entityPointer @@ -839,15 +639,6 @@ function Room:attemptDelete(mouseX, mouseY, color) for index = #self.doors, 1, -1 do if removedEntities[self.doors[index]] then table.remove(self.doors, index) end end - for index = #self.wiredDoors, 1, -1 do - if removedEntities[self.wiredDoors[index]] then table.remove(self.wiredDoors, index) end - end - for index = #self.wiredSwitches, 1, -1 do - if self.wiredSwitches[index]:occupiesCell(cell) and - (not color or color == "white" or self.wiredSwitches[index].color:getname() == color) then - table.remove(self.wiredSwitches, index) - end - end -- NPCs are stamped into every collision layer, but are kept in their own -- registry rather than in self.collideables. Remove them directly so a -- right-click works no matter which editor colour is currently selected. @@ -903,7 +694,7 @@ end function Room:deserialize(saveTable) for _, obj in ipairs(saveTable.objects or {}) do - self:createEntity(obj.class, obj.color, obj.x, obj.y, obj) + self:createEntity(obj.class, obj.color, obj.x, obj.y) -- createEntity turns every loaded player on; honour a saved active flag so -- copies that were off at save time stay off (older savs lack the field and -- keep the old all-on behaviour). @@ -911,11 +702,6 @@ function Room:deserialize(saveTable) self:setActiveColor(obj.color, obj.active) end end - for _, wire in ipairs(saveTable.wires or {}) do - if wire.x and wire.y and wire.circuit then - self:setWireMask({x = wire.x, y = wire.y}, wire.circuit, wire.edges or 0) - end - end end function Room:save() @@ -936,43 +722,12 @@ function Room:saveToSource(name, savePlayerPositions) return writeToSource("rooms/" .. name .. ".sav", room) end -function Room:serializeWires() - local wires = {} - for y, row in pairs(self.wires) do - for x, tracks in pairs(row) do - for circuitId, edges in pairs(tracks) do - table.insert(wires, {x = x, y = y, circuit = circuitId, edges = edges}) - end - end - end - table.sort(wires, function(a, b) - if a.y ~= b.y then return a.y < b.y end - if a.x ~= b.x then return a.x < b.x end - return a.circuit < b.circuit - end) - return wires -end - --- Cross-room wire editing may touch a neighbour loaded from runtime state. --- Merge only its authored wire slice so saving a route cannot accidentally --- bake that neighbour's runtime block/player positions into the source level. -function Room:saveWiresToSource() - if not self.name then return false end - local raw = readFromSource("rooms/" .. self.name .. ".sav") - local ok, saveTable = raw and pcall(TSerial.unpack, raw) - if not ok or type(saveTable) ~= "table" then saveTable = self:serialize(false) end - saveTable.wires = self:serializeWires() - return writeToSource("rooms/" .. self.name .. ".sav", TSerial.pack(saveTable, nil, true)) -end - function Room:serialize(includePlayers) local objects = {} local function emit(class, color, entity) local pos = entity:getGridPos() - 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) + table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y }) end if includePlayers ~= false then @@ -1005,19 +760,15 @@ function Room:serialize(includePlayers) emit("switch", color, switch) end end - for _, switch in ipairs(self.wiredSwitches) do - emit("wired_switch", switch.color:getname(), switch) - end for _, npc in ipairs(self.npcs) do table.insert(objects, { class = npc.saveClass or "npc", x = npc.x, y = npc.y }) end - return { objects = objects, wires = self:serializeWires() } + return { objects = objects } end function nextRoom(pos) - if gameWorld and currentRoom then return gameWorld:nextRoom(currentRoom, pos) end local exit local newPos = {x = pos.x, y = pos.y} if pos.x > gridWidth then @@ -1040,17 +791,13 @@ function nextRoom(pos) -- Missing map neighbours intentionally wrap back into the room just exited. -- A placed neighbour gets the same carried player state at its opposite edge. - local playerState = {x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()} - currentRoom = gameWorld and gameWorld.levels[nextName or currentName] or Room:new{name = nextName or currentName, runtime = true} - if currentRoom then - currentRoom.activeColors = {red = false, green = false, blue = false} - 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 + currentRoom = Room:new{ + name = nextName or currentName, + runtime = true, + player = { + x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors() + } + } if gameWorld then gameWorld:setLastRoom(currentRoom.name) end if currentRoom.name then setEditorRoomName(currentRoom.name) end diff --git a/rooms/room_08_13.sav b/rooms/room_08_13.sav deleted file mode 100644 index 4d12216..0000000 --- a/rooms/room_08_13.sav +++ /dev/null @@ -1,207 +0,0 @@ -{ -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" - } -} -} \ No newline at end of file diff --git a/rooms/room_09_13.sav b/rooms/room_09_13.sav deleted file mode 100644 index d396bf8..0000000 --- a/rooms/room_09_13.sav +++ /dev/null @@ -1,217 +0,0 @@ -{ -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" - } -} -} \ No newline at end of file diff --git a/rooms/room_13_08.sav b/rooms/room_13_08.sav deleted file mode 100644 index 3e784f7..0000000 --- a/rooms/room_13_08.sav +++ /dev/null @@ -1,4 +0,0 @@ -{ -objects={ -} -} \ No newline at end of file diff --git a/rooms/room_14_07.sav b/rooms/room_14_07.sav deleted file mode 100644 index 3e784f7..0000000 --- a/rooms/room_14_07.sav +++ /dev/null @@ -1,4 +0,0 @@ -{ -objects={ -} -} \ No newline at end of file diff --git a/rooms/village_2.sav b/rooms/village_2.sav index 763c8b7..9b14861 100644 --- a/rooms/village_2.sav +++ b/rooms/village_2.sav @@ -1,369 +1,255 @@ { objects={ { - class="door", color="blue", + class="door", + x=6, + y=1 + }, + { + color="red", + class="door", + x=6, + y=1 + }, + { + color="green", + class="door", x=6, y=1 }, { - class="static_wall", color="blue", + class="static_wall", x=1, y=1 }, { + color="red", class="static_wall", + x=1, + y=1 + }, + { + color="green", + class="static_wall", + x=1, + y=1 + }, + { color="blue", + class="static_wall", x=2, y=1 }, { - class="static_wall", - color="blue", - x=3, - y=1 - }, - { - class="static_wall", - color="blue", - x=4, - y=1 - }, - { - class="static_wall", - color="blue", - x=5, - y=1 - }, - { - class="static_wall", - color="blue", - x=7, - y=1 - }, - { - class="static_wall", - color="blue", - x=8, - y=1 - }, - { - class="static_wall", - color="blue", - x=9, - y=1 - }, - { - class="static_wall", - color="blue", - x=10, - y=1 - }, - { - class="static_wall", - color="blue", - x=11, - y=1 - }, - { - class="static_wall", - color="blue", - x=1, - y=11 - }, - { - class="static_wall", - color="blue", - x=11, - y=11 - }, - { - class="spiky_box_2x1_11", - color="blue", - x=1, - y=6 - }, - { - class="box", - color="blue", - x=3, - y=6 - }, - { - class="box_1x2_1-1", - color="blue", - x=4, - y=6 - }, - { - class="spiky_box_1x2_1-1", - color="blue", - x=5, - y=6 - }, - { - class="box_2x2_11-10", - color="blue", - x=6, - y=5 - }, - { - class="spiky_box_2x1_11", - color="blue", - x=7, - y=6 - }, - { - class="spiky_box_2x1_11", - color="blue", - x=9, - y=6 - }, - { - class="door", color="red", - x=6, - y=1 - }, - { class="static_wall", - color="red", - x=1, - y=1 - }, - { - class="static_wall", - color="red", x=2, y=1 }, { - class="static_wall", - color="red", - x=3, - y=1 - }, - { - class="static_wall", - color="red", - x=4, - y=1 - }, - { - class="static_wall", - color="red", - x=5, - y=1 - }, - { - class="static_wall", - color="red", - x=7, - y=1 - }, - { - class="static_wall", - color="red", - x=8, - y=1 - }, - { - class="static_wall", - color="red", - x=9, - y=1 - }, - { - class="static_wall", - color="red", - x=10, - y=1 - }, - { - class="static_wall", - color="red", - x=11, - y=1 - }, - { - class="static_wall", - color="red", - x=1, - y=11 - }, - { - class="static_wall", - color="red", - x=11, - y=11 - }, - { - class="spiky_box_2x1_11", - color="red", - x=1, - y=6 - }, - { - class="box", - color="red", - x=3, - y=6 - }, - { - class="box_1x2_1-1", - color="red", - x=4, - y=6 - }, - { - class="spiky_box_1x2_1-1", - color="red", - x=5, - y=6 - }, - { - class="box_2x2_11-10", - color="red", - x=6, - y=5 - }, - { - class="spiky_box_2x1_11", - color="red", - x=7, - y=6 - }, - { - class="spiky_box_2x1_11", - color="red", - x=9, - y=6 - }, - { - class="door", color="green", - x=6, - y=1 - }, - { class="static_wall", - color="green", - x=1, - y=1 - }, - { - class="static_wall", - color="green", x=2, y=1 }, { + color="blue", class="static_wall", - color="green", x=3, y=1 }, { - class="static_wall", color="green", + class="static_wall", + x=3, + y=1 + }, + { + color="red", + class="static_wall", + x=3, + y=1 + }, + { + color="red", + class="static_wall", x=4, y=1 }, { - class="static_wall", 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", - color="green", x=8, y=1 }, { - class="static_wall", 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 }, { - class="static_wall", 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 }, { - class="static_wall", color="green", + class="static_wall", + x=11, + y=1 + }, + { + color="blue", + class="static_wall", + x=11, + y=1 + }, + { + color="red", + class="static_wall", x=1, y=11 }, { + color="blue", class="static_wall", + x=1, + y=11 + }, + { color="green", + class="static_wall", + x=1, + y=11 + }, + { + color="red", + class="static_wall", x=11, y=11 }, { - class="spiky_box_2x1_11", - color="green", - x=3, - y=6 + color="blue", + class="static_wall", + x=11, + y=11 }, { - class="spiky_box_2x1_11", color="green", - x=1, - y=6 + class="static_wall", + x=11, + y=11 }, { - class="box", - color="green", - x=5, - y=6 - }, - { - class="box_1x2_1-1", - color="green", + color="red", + class="switch", x=6, y=6 }, { - class="spiky_box_1x2_1-1", - color="green", - x=7, + color="blue", + class="switch", + x=6, y=6 }, { - class="box_2x2_11-10", color="green", - x=8, - y=5 - }, - { - class="spiky_box_2x1_11", - color="green", - x=9, - y=6 - }, - { - class="spiky_box_2x1_11", - color="green", - x=11, + class="switch", + x=6, y=6 } } diff --git a/rooms/village_3.sav b/rooms/village_3.sav index 0aaefe2..766b174 100644 --- a/rooms/village_3.sav +++ b/rooms/village_3.sav @@ -1,549 +1,574 @@ { objects={ { - 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=6, - 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=11, - y=2 - }, - { - class="static_wall", - color="blue", - x=11, - y=3 - }, - { - class="static_wall", - color="blue", - x=11, - y=4 - }, - { - class="static_wall", - color="blue", - x=11, - y=5 - }, - { - class="static_wall", - color="blue", - x=11, - y=6 - }, - { - class="static_wall", - color="blue", - x=11, - y=7 - }, - { - class="static_wall", - color="blue", - x=11, - y=8 - }, - { - class="static_wall", - color="blue", - x=11, - y=9 - }, - { - class="static_wall", - color="blue", - x=11, - y=10 - }, - { - class="static_wall", - color="blue", - x=1, - y=11 - }, - { - class="static_wall", - color="blue", - x=2, - y=11 - }, - { - class="static_wall", - color="blue", - x=3, - y=11 - }, - { - class="static_wall", - color="blue", - x=5, - y=11 - }, - { - class="static_wall", - color="blue", - x=6, - y=11 - }, - { - class="static_wall", - color="blue", - x=7, - y=11 - }, - { - class="static_wall", - color="blue", - x=8, - y=11 - }, - { - class="static_wall", - color="blue", - x=9, - y=11 - }, - { - class="static_wall", - color="blue", - x=10, - y=11 - }, - { - class="static_wall", - color="blue", - x=11, - y=11 - }, - { - 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=6, - 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=11, - y=2 - }, - { - class="static_wall", - color="red", - x=11, - y=3 - }, - { - class="static_wall", - color="red", - x=11, - y=4 - }, - { - class="static_wall", - color="red", - x=11, - y=5 - }, - { - class="static_wall", - color="red", - x=11, - y=6 - }, - { - class="static_wall", - color="red", - x=11, - y=7 - }, - { - class="static_wall", - color="red", - x=11, - y=8 - }, - { - class="static_wall", - color="red", - x=11, - y=9 - }, - { - class="static_wall", - color="red", - x=11, - y=10 - }, - { - class="static_wall", - color="red", - x=1, - y=11 - }, - { - class="static_wall", - color="red", - x=2, - y=11 - }, - { - class="static_wall", - color="red", - x=3, - y=11 - }, - { - class="static_wall", - color="red", - x=5, - y=11 - }, - { - class="static_wall", - color="red", - x=6, - y=11 - }, - { - class="static_wall", - color="red", - x=7, - y=11 - }, - { - class="static_wall", - color="red", - x=8, - y=11 - }, - { - class="static_wall", - color="red", - x=9, - y=11 - }, - { - class="static_wall", - color="red", - x=10, - y=11 - }, - { - class="static_wall", - color="red", - x=11, - y=11 - }, - { - class="static_wall", - color="green", - x=1, - y=1 - }, - { - class="static_wall", - color="green", - x=2, - y=1 - }, - { - class="static_wall", - color="green", - x=3, - y=1 - }, - { - class="static_wall", - color="green", - x=4, - y=1 - }, - { - class="static_wall", - color="green", - x=5, - y=1 - }, - { - class="static_wall", - color="green", - x=6, - 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=11, - y=2 - }, - { - class="static_wall", - color="green", - x=11, - y=3 - }, - { - class="static_wall", - color="green", - x=11, - y=4 - }, - { - class="static_wall", - color="green", - x=11, - y=5 - }, - { - class="static_wall", - color="green", - x=11, - y=6 - }, - { - class="static_wall", - color="green", - x=11, - y=7 - }, - { - class="static_wall", - color="green", - x=11, - y=8 - }, - { - class="static_wall", - color="green", - x=11, - y=9 - }, - { - class="static_wall", - color="green", - x=11, - y=10 - }, - { - class="static_wall", - color="green", - x=1, - y=11 - }, - { - class="static_wall", - color="green", - x=2, - y=11 - }, - { - class="static_wall", - color="green", - x=3, - y=11 - }, - { - class="static_wall", - color="green", - x=5, - y=11 - }, - { - class="static_wall", - color="green", - x=6, - y=11 - }, - { - class="static_wall", - color="green", - x=7, - y=11 - }, - { - class="static_wall", - color="green", - x=8, - y=11 - }, - { - class="static_wall", - color="green", - x=9, - y=11 - }, - { - class="static_wall", - color="green", - x=10, - y=11 - }, - { - class="static_wall", - color="green", - x=11, - y=11 - }, - { + y=2, class="npc", x=5, - y=2 + name="slime_woman" + }, + { + y=1, + class="static_wall", + x=1, + color="green" + }, + { + y=1, + class="static_wall", + x=1, + color="blue" + }, + { + y=1, + class="static_wall", + x=1, + color="red" + }, + { + y=1, + class="static_wall", + x=2, + color="red" + }, + { + y=1, + class="static_wall", + x=2, + color="blue" + }, + { + y=1, + class="static_wall", + x=2, + color="green" + }, + { + y=1, + class="static_wall", + x=3, + color="red" + }, + { + y=1, + class="static_wall", + x=3, + color="blue" + }, + { + y=1, + class="static_wall", + x=3, + color="green" + }, + { + y=1, + class="static_wall", + x=4, + color="green" + }, + { + y=1, + class="static_wall", + x=4, + color="red" + }, + { + y=1, + class="static_wall", + x=4, + color="blue" + }, + { + y=1, + class="static_wall", + x=5, + color="red" + }, + { + y=1, + class="static_wall", + x=5, + color="blue" + }, + { + y=1, + class="static_wall", + x=5, + color="green" + }, + { + y=1, + class="static_wall", + x=6, + color="blue" + }, + { + y=1, + class="static_wall", + x=6, + color="red" + }, + { + y=1, + class="static_wall", + x=6, + color="green" + }, + { + y=1, + class="static_wall", + x=7, + color="blue" + }, + { + y=1, + class="static_wall", + x=7, + color="red" + }, + { + y=1, + class="static_wall", + x=7, + color="green" + }, + { + y=1, + class="static_wall", + x=8, + color="green" + }, + { + y=1, + class="static_wall", + x=8, + color="red" + }, + { + y=1, + class="static_wall", + x=8, + color="blue" + }, + { + y=1, + class="static_wall", + x=9, + color="blue" + }, + { + y=1, + class="static_wall", + x=9, + color="green" + }, + { + y=1, + class="static_wall", + x=9, + color="red" + }, + { + y=1, + class="static_wall", + x=10, + color="green" + }, + { + y=1, + class="static_wall", + x=10, + color="blue" + }, + { + y=1, + class="static_wall", + x=10, + color="red" + }, + { + y=1, + class="static_wall", + x=11, + color="green" + }, + { + y=1, + class="static_wall", + x=11, + color="blue" + }, + { + y=1, + class="static_wall", + x=11, + color="red" + }, + { + y=2, + class="static_wall", + x=11, + color="blue" + }, + { + y=2, + class="static_wall", + x=11, + color="red" + }, + { + y=2, + class="static_wall", + x=11, + color="green" + }, + { + y=3, + class="static_wall", + x=11, + color="green" + }, + { + y=3, + class="static_wall", + x=11, + color="blue" + }, + { + y=3, + class="static_wall", + x=11, + color="red" + }, + { + y=4, + class="static_wall", + x=11, + color="blue" + }, + { + y=4, + class="static_wall", + x=11, + color="red" + }, + { + y=4, + class="static_wall", + x=11, + color="green" + }, + { + y=5, + class="static_wall", + x=11, + color="green" + }, + { + y=5, + class="static_wall", + x=11, + color="red" + }, + { + y=5, + class="static_wall", + x=11, + color="blue" + }, + { + y=6, + class="static_wall", + x=11, + color="green" + }, + { + y=6, + class="static_wall", + x=11, + color="red" + }, + { + y=6, + class="static_wall", + x=11, + color="blue" + }, + { + y=7, + class="static_wall", + x=11, + color="blue" + }, + { + y=7, + class="static_wall", + x=11, + color="green" + }, + { + y=7, + class="static_wall", + x=11, + color="red" + }, + { + y=8, + class="static_wall", + x=11, + color="red" + }, + { + y=8, + class="static_wall", + x=11, + color="blue" + }, + { + y=8, + class="static_wall", + x=11, + color="green" + }, + { + y=9, + class="static_wall", + x=11, + color="blue" + }, + { + y=9, + class="static_wall", + x=11, + color="red" + }, + { + y=9, + class="static_wall", + x=11, + color="green" + }, + { + y=10, + class="static_wall", + x=11, + color="blue" + }, + { + y=10, + class="static_wall", + x=11, + color="red" + }, + { + y=10, + class="static_wall", + x=11, + color="green" + }, + { + y=11, + class="static_wall", + x=1, + color="blue" + }, + { + y=11, + class="static_wall", + x=1, + color="green" + }, + { + y=11, + class="static_wall", + x=1, + color="red" + }, + { + y=11, + class="static_wall", + x=2, + color="green" + }, + { + y=11, + class="static_wall", + x=2, + color="blue" + }, + { + y=11, + class="static_wall", + x=2, + color="red" + }, + { + y=11, + class="static_wall", + x=3, + color="red" + }, + { + y=11, + class="static_wall", + x=3, + color="green" + }, + { + y=11, + class="static_wall", + x=3, + color="blue" + }, + { + y=11, + class="static_wall", + x=4, + color="green" + }, + { + y=11, + class="static_wall", + x=4, + color="red" + }, + { + y=11, + class="static_wall", + x=4, + color="blue" + }, + { + y=11, + class="static_wall", + x=5, + color="blue" + }, + { + y=11, + class="static_wall", + x=5, + color="green" + }, + { + y=11, + class="static_wall", + x=5, + color="red" + }, + { + y=11, + class="static_wall", + x=6, + color="blue" + }, + { + y=11, + class="static_wall", + x=6, + color="green" + }, + { + y=11, + class="static_wall", + x=6, + color="red" + }, + { + y=11, + class="static_wall", + x=7, + color="red" + }, + { + y=11, + class="static_wall", + x=7, + color="green" + }, + { + y=11, + class="static_wall", + x=7, + color="blue" + }, + { + y=11, + class="static_wall", + x=8, + color="blue" + }, + { + y=11, + class="static_wall", + x=8, + color="red" + }, + { + y=11, + class="static_wall", + x=8, + color="green" + }, + { + y=11, + class="static_wall", + x=9, + color="red" + }, + { + y=11, + class="static_wall", + x=9, + color="green" + }, + { + y=11, + class="static_wall", + x=9, + color="blue" + }, + { + y=11, + class="static_wall", + x=10, + color="green" + }, + { + y=11, + class="static_wall", + x=10, + color="red" + }, + { + y=11, + class="static_wall", + x=10, + color="blue" + }, + { + y=11, + class="static_wall", + x=11, + color="red" + }, + { + y=11, + class="static_wall", + x=11, + color="blue" + }, + { + y=11, + class="static_wall", + x=11, + color="green" + }, + { + y=2, + class="box", + x=4, + color="blue" } } } \ No newline at end of file diff --git a/rooms/world.sav b/rooms/world.sav index a9bb9de..149089c 100644 --- a/rooms/world.sav +++ b/rooms/world.sav @@ -1,232 +1,212 @@ { +height=18, +lastRoom="white_primary", rooms={ { - x=10, name="3", + x=10, y=10 }, { - x=12, name="5", + x=12, y=10 }, { - x=12, name="village_1", + x=12, y=9 }, { - x=11, - name="village_4", + name="village_3", + x=13, y=8 }, { + name="village_4", x=11, + y=8 + }, + { name="yellow_1", + x=11, y=7 }, { - x=11, name="yellow_3", + x=11, y=6 }, { - x=12, name="yellow_2", + x=12, y=7 }, { - x=11, name="yellow_4", + x=11, y=5 }, { - x=11, name="yellow_5", + x=11, y=4 }, { - x=10, name="yellow_6", + x=10, y=5 }, { - x=1, name="default", - y=1 - }, - { - x=3, - name="old_yellow_3", - y=1 - }, - { - x=5, - name="test_patt", - y=1 - }, - { - x=7, - name="test_patt_update", - y=1 - }, - { - x=9, - name="white_asymmetrical", - y=1 - }, - { - x=11, - name="white_ok", - y=1 - }, - { - x=13, - name="white_primary", - y=1 - }, - { - x=15, - name="yellow_arches", - y=1 - }, - { - x=17, - name="yellow_asymmetrical", - y=1 - }, - { x=1, - name="yellow_cc_moustache", - y=3 + y=1 }, { + name="old_yellow_3", x=3, - name="yellow_color_changing_levvel_1", - y=3 + y=1 }, { + name="test_patt", + x=5, + y=1 + }, + { + name="test_patt_update", + x=7, + y=1 + }, + { + name="white_asymmetrical", + x=9, + y=1 + }, + { + name="white_ok", + x=11, + y=1 + }, + { + name="white_primary", + x=13, + y=1 + }, + { + name="yellow_arches", + x=15, + y=1 + }, + { + name="yellow_asymmetrical", + x=17, + y=1 + }, + { + name="yellow_cc_moustache", + x=1, + y=3 + }, + { + name="yellow_color_changing_levvel_1", + x=3, + y=3 + }, + { + name="yellow_smile", x=7, - name="yellow_smile", y=3 }, { - x=9, name="2", + x=9, y=10 }, { - x=11, name="room_11_09", + x=11, y=9 }, { - x=9, name="yellow_skull", + x=9, y=3 }, { - x=5, name="jorge_room", + x=5, y=5 }, { - x=11, name="4", + x=11, y=10 }, { - x=1, name="room_01_05", + x=1, y=5 }, { - x=2, name="ok", + x=2, y=8 }, { - x=5, name="room_05_18", + x=5, y=18 }, { - x=9, name="village_start", + x=9, y=11 }, { - x=4, name="spiky_test", + x=4, y=8 }, { - x=5, name="corner_trimino_test", - y=7 - }, - { - x=6, - name="tetromino_loop_t", - y=7 - }, - { x=5, + y=7 + }, + { + name="tetromino_loop_t", + x=6, + y=7 + }, + { name="corner_trimino_test_copy", + x=5, y=6 }, { - x=7, name="blocker_test", + x=7, y=7 }, { - x=5, name="spiky_test_2", + x=5, y=8 }, { - x=5, name="spiky_test_3", + x=5, y=9 }, { - x=3, name="jorge2", + x=3, y=14 }, { - x=8, name="room_08_07", - y=7 - }, - { - x=13, - name="village_3", - y=8 - }, - { - x=12, - name="village_2", - y=8 - }, - { - x=14, - name="room_14_07", - y=7 - }, - { - x=9, - name="room_09_13", - y=13 - }, - { x=8, - name="room_08_13", - y=13 + y=7 } }, -lastRoom="room_09_13", -width=18, -height=18 +width=18 } \ No newline at end of file diff --git a/tests/test_wire.lua b/tests/test_wire.lua deleted file mode 100644 index c9bd12a..0000000 --- a/tests/test_wire.lua +++ /dev/null @@ -1,206 +0,0 @@ -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") diff --git a/wire.lua b/wire.lua deleted file mode 100644 index 0ee273c..0000000 --- a/wire.lua +++ /dev/null @@ -1,55 +0,0 @@ -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 diff --git a/wired_door.lua b/wired_door.lua deleted file mode 100644 index 8d30528..0000000 --- a/wired_door.lua +++ /dev/null @@ -1,30 +0,0 @@ -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 diff --git a/wired_switch.lua b/wired_switch.lua deleted file mode 100644 index cc8030d..0000000 --- a/wired_switch.lua +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/world.lua b/world.lua index 6e92bd6..7a4abe7 100644 --- a/world.lua +++ b/world.lua @@ -1,11 +1,8 @@ World = class("World") -local Movement = require "movement" -local Wire = require "wire" - --- 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. +-- The world is deliberately only a layout: rooms continue to own their puzzle +-- state. Keeping the two separate lets the editor move a room without +-- rewriting its level file. function World:initialize() self.width, self.height = 18, 18 self.rooms = {} @@ -13,72 +10,6 @@ function World:initialize() self.previewCache = {} self.lastRoom = nil 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 function World:listRoomNames() @@ -149,13 +80,7 @@ end function World:placeRoom(name, x, y) if not name or not self:isInBounds(x, y) then return false end self:_putRoom(name, x, y) - local saved = self:save() - if self.levels then - self:rebuildCollision() - self:resolveCircuits() - self:rebuildCollision() - end - return saved + return self:save() end function World:removeRoom(x, y) @@ -165,13 +90,7 @@ function World:removeRoom(x, y) self.roomByName[room.name] = nil table.remove(self.rooms, i) if self.lastRoom == room.name then self.lastRoom = nil end - local saved = self:save() - if self.levels then - self:rebuildCollision() - self:resolveCircuits() - self:rebuildCollision() - end - return saved, room.name + return self:save(), room.name end end return false @@ -192,6 +111,7 @@ end function World:setLastRoom(name) if name and self.lastRoom ~= name then self.lastRoom = name + return self:save() end return true end @@ -208,600 +128,6 @@ function World:getNeighbor(name, exit) return self:roomAt(room.x + direction.x, room.y + direction.y) 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) return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1 end @@ -823,8 +149,8 @@ function World:invalidatePreview(name) self.previewCache[name] = nil end -function World:drawRoomPreview(name, x, y, size, roomData) - local room = roomData or self:getRoomData(name) +function World:drawRoomPreview(name, x, y, size) + local room = self:getRoomData(name) if not room then return end local scale = size / (gridWidth * 16) love.graphics.setScissor(x, y, size, size) @@ -863,77 +189,6 @@ function World:drawRoomPreview(name, x, y, size, roomData) love.graphics.setScissor() end -function World:drawWireEditor(centerRoomX, centerRoomY, selectedCircuit) - local roomsAcross = 3 - local roomSize = width / roomsAcross - local cellSize = roomSize / gridWidth - local firstRoomX, firstRoomY = centerRoomX - 1, centerRoomY - 1 - love.graphics.setColor(0.035, 0.035, 0.055, 1) - love.graphics.rectangle("fill", 0, 0, width, height) - love.graphics.setBlendMode("alpha") - love.graphics.setColor(0.28, 0.28, 0.38, 0.28) - love.graphics.setLineWidth(1) - for cell = 0, roomsAcross * gridWidth do - local p = cell * cellSize - love.graphics.line(p, 0, p, height) - love.graphics.line(0, p, width, p) - end - love.graphics.setBlendMode("screen") - - for viewY = 0, roomsAcross - 1 do - for viewX = 0, roomsAcross - 1 do - local roomX, roomY = firstRoomX + viewX, firstRoomY + viewY - local name = self:isInBounds(roomX, roomY) and self:roomAt(roomX, roomY) - local sx, sy = viewX * roomSize, viewY * roomSize - if name then - local level = self.levels[name] - self:drawRoomPreview(name, sx, sy, roomSize, level and level:serialize()) - if level then - level:drawWiresAt(sx, sy, cellSize, selectedCircuit, true) - love.graphics.setBlendMode("alpha") - for _, door in ipairs(level.wiredDoors or {}) do - local pos, color = door:getGridPos(), Wire.color(door.circuitId) - love.graphics.setColor(color[1], color[2], color[3], selectedCircuit == door.circuitId and 1 or 0.65) - love.graphics.setLineWidth(2) - love.graphics.circle("line", sx + (pos.x - 0.5) * cellSize, sy + (pos.y - 0.5) * cellSize, cellSize * 0.22) - end - for _, switch in ipairs(level.wiredSwitches or {}) do - local pos = switch:getGridPos() - love.graphics.setColor(1, 1, 1, switch.activated and 1 or 0.65) - love.graphics.setLineWidth(1) - love.graphics.rectangle("line", sx + (pos.x - 0.68) * cellSize, sy + (pos.y - 0.68) * cellSize, cellSize * 0.36, cellSize * 0.36) - end - love.graphics.setBlendMode("screen") - end - end - love.graphics.setBlendMode("alpha") - love.graphics.setColor(name and 0.8 or 0.18, name and 0.8 or 0.18, name and 0.9 or 0.25, 1) - love.graphics.setLineWidth(viewX == 1 and viewY == 1 and 3 or 1) - love.graphics.rectangle("line", sx, sy, roomSize, roomSize) - if name then - love.graphics.setColor(1, 1, 1, 0.75) - love.graphics.print(name, sx + 4, sy + 3) - end - love.graphics.setBlendMode("screen") - end - end - self.wireEditorProjection = { - firstWorldX = (firstRoomX - 1) * gridWidth + 1, - firstWorldY = (firstRoomY - 1) * gridHeight + 1, - cellSize = cellSize, - } -end - -function World:wireEditorCellAt(mouseX, mouseY, centerRoomX, centerRoomY) - local cellSize = width / 3 / gridWidth - local firstWorldX = (centerRoomX - 2) * gridWidth + 1 - local firstWorldY = (centerRoomY - 2) * gridHeight + 1 - return { - x = firstWorldX + math.floor(mouseX / cellSize), - y = firstWorldY + math.floor(mouseY / cellSize), - } -end - function World:drawHoveredRoomName() local mouseX, mouseY = love.mouse.getPosition() local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)