wired switches and doors
This commit is contained in:
parent
ee7d36e72b
commit
bc4976b324
16 changed files with 2575 additions and 835 deletions
409
world.lua
409
world.lua
|
|
@ -1,10 +1,11 @@
|
|||
World = class("World")
|
||||
|
||||
local Movement = require "movement"
|
||||
local Wire = require "wire"
|
||||
|
||||
-- The world is deliberately only a layout: rooms continue to own their puzzle
|
||||
-- state. Keeping the two separate lets the editor move a room without
|
||||
-- rewriting its level file.
|
||||
-- Rooms own authored slices (objects and wire fragments). World composes those
|
||||
-- slices into authoritative live collision and circuit matrices, so moving a
|
||||
-- room changes placement without rewriting its level file.
|
||||
function World:initialize()
|
||||
self.width, self.height = 18, 18
|
||||
self.rooms = {}
|
||||
|
|
@ -14,6 +15,7 @@ function World:initialize()
|
|||
self:load()
|
||||
self:loadRuntimeState()
|
||||
self.levels = {}
|
||||
self.circuitSerial = 0
|
||||
|
||||
end
|
||||
|
||||
|
|
@ -22,6 +24,8 @@ function World:loadLevels()
|
|||
-- through the same runtime-first loader, not just the initially visible one.
|
||||
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name, runtime = true} end
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
|
||||
-- The editor can replace a level with a freshly loaded authored Room. Keep the
|
||||
|
|
@ -30,9 +34,26 @@ function World:setLevel(room)
|
|||
if not room or not room.name or not self:roomLocation(room.name) then return false end
|
||||
self.levels[room.name] = room
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
return true
|
||||
end
|
||||
|
||||
function World:newCircuitId()
|
||||
local used = {}
|
||||
for _, room in pairs(self.levels or {}) do
|
||||
for _, door in ipairs(room.wiredDoors or {}) do used[door.circuitId] = true end
|
||||
for _, row in pairs(room.wires or {}) do
|
||||
for _, tracks in pairs(row) do for circuitId in pairs(tracks) do used[circuitId] = true end end
|
||||
end
|
||||
end
|
||||
repeat
|
||||
self.circuitSerial = self.circuitSerial + 1
|
||||
local id = string.format("wire_%08x_%04x", os.time() % 0xffffffff, self.circuitSerial)
|
||||
if not used[id] then return id end
|
||||
until false
|
||||
end
|
||||
|
||||
-- Runtime state is one logical world snapshot: each level slice is written to
|
||||
-- LÖVE's save directory, and this companion file says which slice was active.
|
||||
-- The authored world layout remains source-only for the editor.
|
||||
|
|
@ -128,7 +149,13 @@ end
|
|||
function World:placeRoom(name, x, y)
|
||||
if not name or not self:isInBounds(x, y) then return false end
|
||||
self:_putRoom(name, x, y)
|
||||
return self:save()
|
||||
local saved = self:save()
|
||||
if self.levels then
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
return saved
|
||||
end
|
||||
|
||||
function World:removeRoom(x, y)
|
||||
|
|
@ -138,7 +165,13 @@ function World:removeRoom(x, y)
|
|||
self.roomByName[room.name] = nil
|
||||
table.remove(self.rooms, i)
|
||||
if self.lastRoom == room.name then self.lastRoom = nil end
|
||||
return self:save(), room.name
|
||||
local saved = self:save()
|
||||
if self.levels then
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
end
|
||||
return saved, room.name
|
||||
end
|
||||
end
|
||||
return false
|
||||
|
|
@ -238,6 +271,292 @@ function World:roomCell(cell)
|
|||
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
||||
end
|
||||
|
||||
local function cellKey(cell)
|
||||
return cell.x .. ":" .. cell.y
|
||||
end
|
||||
|
||||
function World:isSwitchPressed(room, switch)
|
||||
local color = switch.color:getname()
|
||||
local cell = self:worldCell(room, switch:getGridPos())
|
||||
local row = cell and self.collidableMatrices[color][cell.y]
|
||||
local target = row and row[cell.x]
|
||||
local player = room.player[color]
|
||||
return target ~= nil or (player and player:occupiesCell(switch:getGridPos())) or false
|
||||
end
|
||||
|
||||
function World:rebuildCircuitMatrices()
|
||||
self.wireMatrix = {}
|
||||
self.wiredSwitchMatrix = {}
|
||||
self.wiredDoorMatrix = {}
|
||||
|
||||
local function cellAt(matrix, cell, create)
|
||||
local row = matrix[cell.y]
|
||||
if not row and create then row = {}; matrix[cell.y] = row end
|
||||
local value = row and row[cell.x]
|
||||
return value, row
|
||||
end
|
||||
local function addDevice(matrix, cell, device, room)
|
||||
local devices, row = cellAt(matrix, cell, true)
|
||||
if not devices then devices = {}; row[cell.x] = devices end
|
||||
table.insert(devices, {entity = device, room = room})
|
||||
end
|
||||
|
||||
for name, room in pairs(self.levels or {}) do
|
||||
if self:roomLocation(name) then
|
||||
for _, switch in ipairs(room.wiredSwitches or {}) do
|
||||
addDevice(self.wiredSwitchMatrix, self:worldCell(room, switch:getGridPos()), switch, room)
|
||||
end
|
||||
for _, door in ipairs(room.wiredDoors or {}) do
|
||||
-- A terminal painted beneath the door is authoritative. This also
|
||||
-- repairs older white doors whose RGB layers were assigned separate
|
||||
-- IDs before layered placement shared a circuit.
|
||||
door.circuitId = room:wireCircuitAt(door:getGridPos()) or door.circuitId
|
||||
addDevice(self.wiredDoorMatrix, self:worldCell(room, door:getGridPos()), door, room)
|
||||
end
|
||||
for y, sourceRow in pairs(room.wires or {}) do
|
||||
for x, sourceTracks in pairs(sourceRow) do
|
||||
local worldCell = self:worldCell(room, {x = x, y = y})
|
||||
local tracks, row = cellAt(self.wireMatrix, worldCell, true)
|
||||
if not tracks then tracks = {}; row[worldCell.x] = tracks end
|
||||
for circuitId, mask in pairs(sourceTracks) do
|
||||
tracks[circuitId] = {mask = mask, room = room, localCell = {x = x, y = y}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Rebuild the electrical graph from room-local fragments projected onto the
|
||||
-- world grid. IDs keep overlapping routes distinct; reciprocal direction bits
|
||||
-- are what actually make two neighbouring cells electrically continuous.
|
||||
function World:resolveCircuits()
|
||||
if not self.collidableMatrices then return end
|
||||
self:rebuildCircuitMatrices()
|
||||
local circuits = {}
|
||||
local switchesAt = {}
|
||||
local switchPressed = {}
|
||||
|
||||
for y, row in pairs(self.wiredSwitchMatrix) do
|
||||
for x, devices in pairs(row) do
|
||||
local key = cellKey({x = x, y = y})
|
||||
for _, device in ipairs(devices) do
|
||||
local switch, room = device.entity, device.room
|
||||
local pressed = self:isSwitchPressed(room, switch)
|
||||
switchPressed[switch] = pressed
|
||||
-- Entity flags are derived caches used by the existing sprite API.
|
||||
-- World.circuitState below remains the authoritative logical state.
|
||||
if pressed then switch:activate() else switch:deactivate() end
|
||||
switchesAt[key] = switchesAt[key] or {}
|
||||
table.insert(switchesAt[key], switch)
|
||||
end
|
||||
end
|
||||
end
|
||||
for y, row in pairs(self.wireMatrix) do
|
||||
for x, tracks in pairs(row) do
|
||||
local worldCell = {x = x, y = y}
|
||||
local key = cellKey(worldCell)
|
||||
for circuitId, track in pairs(tracks) do
|
||||
circuits[circuitId] = circuits[circuitId] or {}
|
||||
circuits[circuitId][key] = {cell = worldCell, mask = track.mask}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local components = {}
|
||||
for circuitId, nodes in pairs(circuits) do
|
||||
components[circuitId] = {}
|
||||
local visited = {}
|
||||
for key, node in pairs(nodes) do
|
||||
if not visited[key] then
|
||||
local component = {
|
||||
switches = {}, nodes = {}, switchColors = {red = false, green = false, blue = false},
|
||||
terminalCells = {}, valid = false,
|
||||
}
|
||||
local pending, nextNode = {node}, 1
|
||||
visited[key] = component
|
||||
while nextNode <= #pending do
|
||||
local current = pending[nextNode]
|
||||
nextNode = nextNode + 1
|
||||
local currentKey = cellKey(current.cell)
|
||||
component.nodes[currentKey] = true
|
||||
local degree = 0
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if Wire.has(current.mask, direction.bit) then
|
||||
local adjacentKey = cellKey({x = current.cell.x + direction.x, y = current.cell.y + direction.y})
|
||||
local adjacent = nodes[adjacentKey]
|
||||
if adjacent and Wire.has(adjacent.mask, direction.opposite) then
|
||||
degree = degree + 1
|
||||
if not visited[adjacentKey] then
|
||||
visited[adjacentKey] = component
|
||||
table.insert(pending, adjacent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- A switch connects only when the run literally terminates on its
|
||||
-- cell. Merely crossing or passing through a switch does not claim it.
|
||||
if degree <= 1 and switchesAt[currentKey] then
|
||||
component.terminalCells[currentKey] = switchesAt[currentKey]
|
||||
end
|
||||
end
|
||||
local terminalCount, terminalSwitches = 0
|
||||
for _, terminal in pairs(component.terminalCells) do
|
||||
terminalCount = terminalCount + 1
|
||||
terminalSwitches = terminal
|
||||
end
|
||||
-- Multiple RGB entities at one cell are one logical (possibly white)
|
||||
-- switch. Two different switch cells can never belong to one wire run.
|
||||
component.valid = terminalCount == 1
|
||||
component.terminalCount = terminalCount
|
||||
if component.valid then
|
||||
for _, switch in ipairs(terminalSwitches) do
|
||||
component.switches[switch] = true
|
||||
component.switchColors[switch.color:getname()] = true
|
||||
end
|
||||
end
|
||||
components[circuitId][key] = component
|
||||
for componentKey in pairs(component.nodes) do components[circuitId][componentKey] = component end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local doorPowered = {}
|
||||
for y, row in pairs(self.wiredDoorMatrix) do
|
||||
for x, devices in pairs(row) do
|
||||
for _, device in ipairs(devices) do
|
||||
local door = device.entity
|
||||
local position = {x = x, y = y}
|
||||
local key = cellKey(position)
|
||||
local tracks = self.wireMatrix[y] and self.wireMatrix[y][x]
|
||||
local runCount, powered = 0, tracks ~= nil
|
||||
for circuitId in pairs(tracks or {}) do
|
||||
runCount = runCount + 1
|
||||
local component = components[circuitId] and components[circuitId][key]
|
||||
if not component or not component.valid then
|
||||
powered = false
|
||||
else
|
||||
for switch in pairs(component.switches) do
|
||||
if not switchPressed[switch] then powered = false end
|
||||
end
|
||||
end
|
||||
end
|
||||
powered = powered and runCount > 0
|
||||
doorPowered[door] = powered
|
||||
-- locked/collision is a projection of world state onto the physical
|
||||
-- room entity so the existing movement renderer can consume it.
|
||||
door:setPowered(powered)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.circuits = circuits
|
||||
self.circuitComponents = components
|
||||
self.circuitState = {
|
||||
switchPressed = switchPressed,
|
||||
doorPowered = doorPowered,
|
||||
components = components,
|
||||
}
|
||||
end
|
||||
|
||||
function World:wireSwitchColorsAt(room, localCell, circuitId)
|
||||
local worldCell = self:worldCell(room, localCell)
|
||||
local component = worldCell and self.circuitComponents and
|
||||
self.circuitComponents[circuitId] and self.circuitComponents[circuitId][cellKey(worldCell)]
|
||||
if not component or not component.valid then return nil end
|
||||
local colors = component.switchColors
|
||||
if not colors.red and not colors.green and not colors.blue then return nil end
|
||||
return colors
|
||||
end
|
||||
|
||||
function World:wireColorAt(room, localCell, circuitId, visibleColors)
|
||||
local colors = self:wireSwitchColorsAt(room, localCell, circuitId)
|
||||
if not colors then return nil end
|
||||
visibleColors = visibleColors or {red = true, green = true, blue = true}
|
||||
local color = {
|
||||
colors.red and visibleColors.red and 1 or 0,
|
||||
colors.green and visibleColors.green and 1 or 0,
|
||||
colors.blue and visibleColors.blue and 1 or 0,
|
||||
}
|
||||
if color[1] == 0 and color[2] == 0 and color[3] == 0 then return nil end
|
||||
return {
|
||||
color[1], color[2], color[3],
|
||||
}
|
||||
end
|
||||
|
||||
function World:ensureWireAt(cell, circuitId)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return {} end
|
||||
local tracks = room:wireTracksAt(localCell, true)
|
||||
if tracks[circuitId] == nil then tracks[circuitId] = 0 end
|
||||
self:resolveCircuits()
|
||||
return {room}
|
||||
end
|
||||
|
||||
function World:addWireConnection(from, to, circuitId)
|
||||
local direction = Wire.directionBetween(from, to)
|
||||
if not direction then return {} end
|
||||
local fromRoom, fromCell = self:roomCell(from)
|
||||
local toRoom, toCell = self:roomCell(to)
|
||||
if not fromRoom or not toRoom then return {} end
|
||||
local fromTracks = fromRoom:wireTracksAt(fromCell, true)
|
||||
local toTracks = toRoom:wireTracksAt(toCell, true)
|
||||
local oldFrom, oldTo = fromTracks[circuitId], toTracks[circuitId]
|
||||
fromTracks[circuitId] = Wire.add(fromTracks[circuitId], direction.bit)
|
||||
toTracks[circuitId] = Wire.add(toTracks[circuitId], direction.opposite)
|
||||
self:resolveCircuits()
|
||||
local component = self.circuitComponents[circuitId] and self.circuitComponents[circuitId][cellKey(from)]
|
||||
if component and component.terminalCount > 1 then
|
||||
fromTracks[circuitId], toTracks[circuitId] = oldFrom, oldTo
|
||||
if oldFrom == nil and not next(fromTracks) then fromRoom.wires[fromCell.y][fromCell.x] = nil end
|
||||
if oldTo == nil and not next(toTracks) then toRoom.wires[toCell.y][toCell.x] = nil end
|
||||
self:resolveCircuits()
|
||||
return {}, "A wire run can terminate at only one switch. Start another run from the door."
|
||||
end
|
||||
return fromRoom == toRoom and {fromRoom} or {fromRoom, toRoom}
|
||||
end
|
||||
|
||||
function World:removeWireAt(cell, circuitId)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return {} end
|
||||
local tracks = room:wireTracksAt(localCell)
|
||||
local mask = tracks and tracks[circuitId]
|
||||
if mask == nil then return {} end
|
||||
local changed, seen = {room}, {[room] = true}
|
||||
for _, direction in ipairs(Wire.directions) do
|
||||
if Wire.has(mask, direction.bit) then
|
||||
local neighborRoom, neighborCell = self:roomCell({x = cell.x + direction.x, y = cell.y + direction.y})
|
||||
local neighborTracks = neighborRoom and neighborRoom:wireTracksAt(neighborCell)
|
||||
if neighborTracks and neighborTracks[circuitId] ~= nil then
|
||||
neighborTracks[circuitId] = Wire.remove(neighborTracks[circuitId], direction.opposite)
|
||||
if not seen[neighborRoom] then seen[neighborRoom] = true; table.insert(changed, neighborRoom) end
|
||||
end
|
||||
end
|
||||
end
|
||||
room:removeWire(localCell, circuitId)
|
||||
self:resolveCircuits()
|
||||
return changed
|
||||
end
|
||||
|
||||
function World:wiredDoorAt(cell)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
if not room then return nil end
|
||||
for _, door in ipairs(room.wiredDoors or {}) do
|
||||
if door:occupiesCell(localCell) then return door, room end
|
||||
end
|
||||
end
|
||||
|
||||
function World:circuitAt(cell, preferredCircuit)
|
||||
local room, localCell = self:roomCell(cell)
|
||||
local tracks = room and room:wireTracksAt(localCell)
|
||||
if not tracks then return nil end
|
||||
if preferredCircuit and tracks[preferredCircuit] ~= nil then return preferredCircuit end
|
||||
local ids = {}
|
||||
for circuitId in pairs(tracks) do table.insert(ids, circuitId) end
|
||||
table.sort(ids)
|
||||
return ids[#ids]
|
||||
end
|
||||
|
||||
function World:movementContext(room, color)
|
||||
local proxies = {}
|
||||
local world = self
|
||||
|
|
@ -373,6 +692,8 @@ end
|
|||
function World:tick(room)
|
||||
room:tickLocal()
|
||||
self:rebuildCollision()
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
self:nextRoomCheck(room)
|
||||
end
|
||||
|
||||
|
|
@ -421,6 +742,8 @@ function World:nextRoom(room, pos)
|
|||
end
|
||||
self:playerJoinCheck(currentRoom)
|
||||
self.lastRoom = currentRoom.name
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
setEditorRoomName(currentRoom.name)
|
||||
end
|
||||
|
||||
|
|
@ -451,6 +774,7 @@ function World:moveGroup(group, direction)
|
|||
end
|
||||
self:rebuildCollision()
|
||||
for room in pairs(changed) do room:resolveSwitchesAndDoors() end
|
||||
self:resolveCircuits()
|
||||
self:rebuildCollision()
|
||||
for room in pairs(changed) do self:markDirty(room) end
|
||||
end
|
||||
|
|
@ -499,8 +823,8 @@ function World:invalidatePreview(name)
|
|||
self.previewCache[name] = nil
|
||||
end
|
||||
|
||||
function World:drawRoomPreview(name, x, y, size)
|
||||
local room = self:getRoomData(name)
|
||||
function World:drawRoomPreview(name, x, y, size, roomData)
|
||||
local room = roomData or self:getRoomData(name)
|
||||
if not room then return end
|
||||
local scale = size / (gridWidth * 16)
|
||||
love.graphics.setScissor(x, y, size, size)
|
||||
|
|
@ -539,6 +863,77 @@ function World:drawRoomPreview(name, x, y, size)
|
|||
love.graphics.setScissor()
|
||||
end
|
||||
|
||||
function World:drawWireEditor(centerRoomX, centerRoomY, selectedCircuit)
|
||||
local roomsAcross = 3
|
||||
local roomSize = width / roomsAcross
|
||||
local cellSize = roomSize / gridWidth
|
||||
local firstRoomX, firstRoomY = centerRoomX - 1, centerRoomY - 1
|
||||
love.graphics.setColor(0.035, 0.035, 0.055, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setColor(0.28, 0.28, 0.38, 0.28)
|
||||
love.graphics.setLineWidth(1)
|
||||
for cell = 0, roomsAcross * gridWidth do
|
||||
local p = cell * cellSize
|
||||
love.graphics.line(p, 0, p, height)
|
||||
love.graphics.line(0, p, width, p)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
|
||||
for viewY = 0, roomsAcross - 1 do
|
||||
for viewX = 0, roomsAcross - 1 do
|
||||
local roomX, roomY = firstRoomX + viewX, firstRoomY + viewY
|
||||
local name = self:isInBounds(roomX, roomY) and self:roomAt(roomX, roomY)
|
||||
local sx, sy = viewX * roomSize, viewY * roomSize
|
||||
if name then
|
||||
local level = self.levels[name]
|
||||
self:drawRoomPreview(name, sx, sy, roomSize, level and level:serialize())
|
||||
if level then
|
||||
level:drawWiresAt(sx, sy, cellSize, selectedCircuit, true)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
for _, door in ipairs(level.wiredDoors or {}) do
|
||||
local pos, color = door:getGridPos(), Wire.color(door.circuitId)
|
||||
love.graphics.setColor(color[1], color[2], color[3], selectedCircuit == door.circuitId and 1 or 0.65)
|
||||
love.graphics.setLineWidth(2)
|
||||
love.graphics.circle("line", sx + (pos.x - 0.5) * cellSize, sy + (pos.y - 0.5) * cellSize, cellSize * 0.22)
|
||||
end
|
||||
for _, switch in ipairs(level.wiredSwitches or {}) do
|
||||
local pos = switch:getGridPos()
|
||||
love.graphics.setColor(1, 1, 1, switch.activated and 1 or 0.65)
|
||||
love.graphics.setLineWidth(1)
|
||||
love.graphics.rectangle("line", sx + (pos.x - 0.68) * cellSize, sy + (pos.y - 0.68) * cellSize, cellSize * 0.36, cellSize * 0.36)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
end
|
||||
end
|
||||
love.graphics.setBlendMode("alpha")
|
||||
love.graphics.setColor(name and 0.8 or 0.18, name and 0.8 or 0.18, name and 0.9 or 0.25, 1)
|
||||
love.graphics.setLineWidth(viewX == 1 and viewY == 1 and 3 or 1)
|
||||
love.graphics.rectangle("line", sx, sy, roomSize, roomSize)
|
||||
if name then
|
||||
love.graphics.setColor(1, 1, 1, 0.75)
|
||||
love.graphics.print(name, sx + 4, sy + 3)
|
||||
end
|
||||
love.graphics.setBlendMode("screen")
|
||||
end
|
||||
end
|
||||
self.wireEditorProjection = {
|
||||
firstWorldX = (firstRoomX - 1) * gridWidth + 1,
|
||||
firstWorldY = (firstRoomY - 1) * gridHeight + 1,
|
||||
cellSize = cellSize,
|
||||
}
|
||||
end
|
||||
|
||||
function World:wireEditorCellAt(mouseX, mouseY, centerRoomX, centerRoomY)
|
||||
local cellSize = width / 3 / gridWidth
|
||||
local firstWorldX = (centerRoomX - 2) * gridWidth + 1
|
||||
local firstWorldY = (centerRoomY - 2) * gridHeight + 1
|
||||
return {
|
||||
x = firstWorldX + math.floor(mouseX / cellSize),
|
||||
y = firstWorldY + math.floor(mouseY / cellSize),
|
||||
}
|
||||
end
|
||||
|
||||
function World:drawHoveredRoomName()
|
||||
local mouseX, mouseY = love.mouse.getPosition()
|
||||
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue