Prevent doors closing through occupants

This commit is contained in:
Your Name 2026-08-12 00:21:53 -04:00
parent bf09a8844a
commit 9731202739
5 changed files with 108 additions and 6 deletions

View file

@ -11,6 +11,7 @@ function Door:initialize(x, y, color)
collision = "unmoveable", collision = "unmoveable",
sprite = loadSprite("art/game/door_" .. color:sub(1, 1) .. ".png"), sprite = loadSprite("art/game/door_" .. color:sub(1, 1) .. ".png"),
}) })
self.isDoor = true
self.locked = true self.locked = true
self:refreshSprites() self:refreshSprites()
end end
@ -27,10 +28,10 @@ function Door:refreshSprites()
self.sprites = { [self.doorColor] = sprite } self.sprites = { [self.doorColor] = sprite }
end end
function Door:setColors(colors) function Door:setColors(colors, canClose)
if colors[self.doorColor] then if colors[self.doorColor] then
if self.locked then self:unlock() end if self.locked then self:unlock() end
else elseif canClose ~= false then
if not self.locked then self:lock() end if not self.locked then self:lock() end
end end

View file

@ -538,10 +538,29 @@ end
function Room:resolveSwitchesAndDoors() function Room:resolveSwitchesAndDoors()
local setDoorColors = self:switchCheck() local setDoorColors = self:switchCheck()
for _, door in pairs(self.doors) do for _, door in pairs(self.doors) do
door:setColors(setDoorColors) door:setColors(setDoorColors, self:doorCanClose(door))
end end
end end
-- Match World:doorCanClose for ordinary room-local doors. All colour layers
-- count, while other layers of the same doorway do not obstruct each other.
function Room:doorCanClose(door)
for _, cell in ipairs(door:getOccupiedCells()) do
for _, player in pairs(self.player or {}) do
if player:occupiesCell(cell) then return false end
end
for _, entities in pairs(self.collideables or {}) do
for _, entity in ipairs(entities) do
if entity ~= door and not entity.isDoor and entity:occupiesCell(cell) and
not (entity.canPassOver and entity:canPassOver()) then
return false
end
end
end
end
return true
end
function Room:playerJoinCheck(previousPositions, newPositions) function Room:playerJoinCheck(previousPositions, newPositions)
local activate = {} local activate = {}
local deactivate = {} local deactivate = {}

View file

@ -192,6 +192,66 @@ test("an orphaned stub at a door does not block a valid switch run", function()
assertTrue(wiredDoor.powered) assertTrue(wiredDoor.powered)
end) end)
test("real wired and regular doors stay open while any player or collidable blocks the doorway", function()
local oldLoadSprite = loadSprite
loadSprite = function()
return {getWidth = function() return 16 end, getHeight = function() return 16 end}
end
require "color"
require "wired_door"
local function occupant(x, y)
local entity = {x = x, y = y}
function entity:occupiesCell(cell) return self.x == cell.x and self.y == cell.y end
function entity:canPassOver() return false end
return entity
end
local wired = WiredDoor:new(2, 2, "blue", "circuit_a")
local ordinary = Door:new(4, 2, "red")
local room = {
player = {green = occupant(2, 2)},
collideables = {
red = {ordinary}, green = {occupant(4, 2)}, blue = {wired},
},
}
local world = setmetatable({}, {__index = World})
wired:setPowered(true)
assertFalse(wired.locked, "wired door did not open")
assertFalse(world:doorCanClose(room, wired), "green player did not block blue wired door")
wired:setPowered(false, world:doorCanClose(room, wired))
assertFalse(wired.locked, "wired door shut on player")
ordinary:setColors({red = true})
assertFalse(ordinary.locked, "regular door did not open")
assertFalse(world:doorCanClose(room, ordinary), "green collidable did not block red regular door")
ordinary:setColors({red = false}, world:doorCanClose(room, ordinary))
assertFalse(ordinary.locked, "regular door shut on collidable")
room.player.green.x, room.player.green.y = 1, 1
room.collideables.green[1].x, room.collideables.green[1].y = 1, 1
wired:setPowered(false, world:doorCanClose(room, wired))
ordinary:setColors({red = false}, world:doorCanClose(room, ordinary))
assertTrue(wired.locked, "wired door did not close once clear")
assertTrue(ordinary.locked, "regular door did not close once clear")
loadSprite = oldLoadSprite
end)
test("circuit resolution passes doorway occupancy to a wired door", function()
local world, roomA, _, wiredDoor = makeWorld()
local player = {x = 1, y = 1}
function player:occupiesCell(cell) return self.x == cell.x and self.y == cell.y end
roomA.player.green = player
function wiredDoor:setPowered(powered, canClose)
self.powered, self.canClose = powered, canClose
end
world:resolveCircuits()
assertFalse(wiredDoor.powered, "unpressed switch unexpectedly powered door")
assertFalse(wiredDoor.canClose, "resolver omitted player doorway obstruction")
end)
test("selecting a parked colour changes to its room", function() test("selecting a parked colour changes to its room", function()
local function player() local function player()
local entity = {switched = false} local entity = {switched = false}

View file

@ -10,10 +10,10 @@ function WiredDoor:initialize(x, y, color, circuitId)
self.wired = true self.wired = true
end end
function WiredDoor:setPowered(powered) function WiredDoor:setPowered(powered, canClose)
if powered then if powered then
if self.locked then self:unlock() end if self.locked then self:unlock() end
elseif not self.locked then elseif canClose ~= false and not self.locked then
self:lock() self:lock()
end end
self:refreshSprites() self:refreshSprites()

View file

@ -284,6 +284,28 @@ function World:isSwitchPressed(room, switch)
return target ~= nil or (player and player:occupiesCell(switch:getGridPos())) or false return target ~= nil or (player and player:occupiesCell(switch:getGridPos())) or false
end end
-- Check every rendered/colliding colour layer. A red or green player/box can
-- visibly occupy a blue door cell, so checking only the door's own layer lets
-- wired doors close through an apparent occupant. Other door layers at the
-- same location are intentionally ignored: they are one layered doorway.
function World:doorCanClose(room, door)
local cells = door.getOccupiedCells and door:getOccupiedCells() or {door:getGridPos()}
for _, cell in ipairs(cells) do
for _, player in pairs(room.player or {}) do
if player:occupiesCell(cell) then return false end
end
for _, entities in pairs(room.collideables or {}) do
for _, entity in ipairs(entities) do
if entity ~= door and not entity.isDoor and entity:occupiesCell(cell) and
not (entity.canPassOver and entity:canPassOver()) then
return false
end
end
end
end
return true
end
function World:rebuildCircuitMatrices() function World:rebuildCircuitMatrices()
self.wireMatrix = {} self.wireMatrix = {}
self.wiredSwitchMatrix = {} self.wiredSwitchMatrix = {}
@ -448,7 +470,7 @@ function World:resolveCircuits()
doorPowered[door] = powered doorPowered[door] = powered
-- locked/collision is a projection of world state onto the physical -- locked/collision is a projection of world state onto the physical
-- room entity so the existing movement renderer can consume it. -- room entity so the existing movement renderer can consume it.
door:setPowered(powered) door:setPowered(powered, self:doorCanClose(device.room, door))
end end
end end
end end