player blocker

This commit is contained in:
Your Name 2026-08-11 20:29:35 -04:00
parent 0618cfc51d
commit a60f46d585
12 changed files with 1141 additions and 89 deletions

View file

@ -0,0 +1 @@
{class="player_blocker"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View file

@ -156,6 +156,10 @@ function Entity:canPassOver()
return false
end
function Entity:blocksPlayer()
return false
end
function Entity:getClass()
return self.class.name
end

View file

@ -0,0 +1,21 @@
require "entity"
-- A visual boundary that only affects player movement. It deliberately stays
-- out of the ordinary collidable matrix, so moveable blocks can occupy and
-- pass through the same cell.
GenericPlayerBlocker = class("GenericPlayerBlocker", Entity)
function GenericPlayerBlocker:initialize(x, y, color, sprite, cellShape)
Entity.initialize(self, {
x = x,
y = y,
color = color,
collision = "none",
sprite = sprite,
cellShape = cellShape
})
end
function GenericPlayerBlocker:blocksPlayer()
return true
end

View file

@ -120,6 +120,9 @@ local function rebuildBoxPaletteSections()
table.sort(names)
table.insert(boxPaletteSections, { prefix = prefix, names = names })
end
if gameAssets.player_blocker then
table.insert(boxPaletteSections, { prefix = "barrier", names = {"player_blocker"} })
end
end
local function buildBoxPicker(rect)
@ -1124,7 +1127,7 @@ function editorDraw()
end
end
love.graphics.rectangle("line", s.searchBox.x, s.searchBox.y, s.searchBox.w, s.searchBox.h)
love.graphics.printf(boxPaletteOpen and "BOX VARIANTS" or "SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left")
love.graphics.printf(boxPaletteOpen and "QUICK PALETTE" or "SEARCH: " .. assetSearchText, s.searchBox.x + 6, s.searchBox.y + 9, s.searchBox.w - 12, "left")
if s.boxPicker then
drawBoxPicker(s.boxPicker)

View file

@ -14,6 +14,7 @@ require "generic_sticky"
require "generic_skewer"
require "generic_spiky"
require "generic_unmoveable"
require "generic_player_blocker"
local Movement = require "movement"
@ -22,6 +23,7 @@ function Room:initialize(t)
self.switches = { red = {}, green = {}, blue = {} }
self.doors = {}
self.collideables = { red = {}, green = {}, blue = {} }
self.playerBlockers = { red = {}, green = {}, blue = {} }
self.npcs = {}
self.activeColors = {red = false, green = false, blue = false}
self.colorsPlayerHas = {}
@ -104,8 +106,10 @@ function Room:initialize(t)
self.switchMatrices = {}
self.collidableMatrices = {} --{red = {{...}{...}{...}}, ...
self.playerBlockerMatrices = {}
self:createSwitchMatrix()
self:createCollideablesMatrix()
self:createPlayerBlockerMatrix()
-- Resolve switches/doors up front so the very first frame renders correct
-- lock state: a block already sitting on a switch presses it before anything
@ -128,6 +132,7 @@ 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
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 _, npc in ipairs(self.npcs) do npc:draw(channel) end
end
@ -153,6 +158,12 @@ function Room:createSwitchMatrix()
end
end
function Room:createPlayerBlockerMatrix()
for color, blockers in pairs(self.playerBlockers) do
self.playerBlockerMatrices[color] = self:createEntityMatrix(blockers)
end
end
function Room:createEntityMatrix(entities)
local matrix = {}
@ -220,10 +231,16 @@ function Room:movePlayer(key)
end
if initialTargetEntity then initialTargetEntity:bump() end
local playerBlocker
if self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then
playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell)
end
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
else
move, entitiesToPush = Movement.findMovableGroup(
{initialTargetCell}, dir,
@ -351,6 +368,7 @@ function Room:tick()
end
end
self:createCollideablesMatrix()
self:createPlayerBlockerMatrix()
self:createSwitchMatrix()
self:resolveSwitchesAndDoors()
self:nextRoomCheck()
@ -466,7 +484,7 @@ end
-- Instantiate one object from the flat {class,color,x,y} schema. `class` names an
-- art asset, and its .meta declares the behaviour ("door"/"switch"/"player"/
-- "moveable"/"sticky"/"skewer"/"spiky"); anything without a .meta defaults to immoveable. npc is the one
-- "moveable"/"sticky"/"skewer"/"spiky"/"player_blocker"); anything without a .meta defaults to immoveable. npc is the one
-- keyword handled directly since it has no art.
function Room:createEntity(class, color, x, y)
if class == "npc" then
@ -509,7 +527,10 @@ function Room:createEntity(class, color, x, y)
-- collides on its real cells and its gaps read as empty space.
local cellShape = props.cells and props.cells[color]
if cellShape and #cellShape == 0 then cellShape = nil end
if behavior == "moveable" then
if behavior == "player_blocker" then
entity = GenericPlayerBlocker:new(x, y, color, sprite, cellShape)
self:registerPlayerBlocker(entity, color)
elseif behavior == "moveable" then
entity = GenericMoveable:new(x, y, color, sprite, class, cellShape)
elseif behavior == "sticky" then
entity = GenericSticky:new(x, y, color, sprite, class, cellShape)
@ -520,7 +541,9 @@ function Room:createEntity(class, color, x, y)
else
entity = GenericUnmoveable:new(x, y, color, sprite, cellShape)
end
self:registerCollidable(entity, {color})
if behavior ~= "player_blocker" then
self:registerCollidable(entity, {color})
end
end
if entity then entity.saveClass = class end
@ -534,6 +557,10 @@ function Room:registerCollidable(entityPointer, colorArray)
end
end
function Room:registerPlayerBlocker(entityPointer, color)
table.insert(self.playerBlockers[color], entityPointer)
end
function Room:registerDoor(entityPointer, color)
table.insert(self.doors, entityPointer)
self:registerCollidable(entityPointer, color)
@ -591,6 +618,14 @@ function Room:attemptDelete(mouseX, mouseY, color)
table.remove(entityArray, index)
end
end
-- Player-only blockers are rendered entities, but use their own collision
-- matrix so blocks can overlap them.
local blockerArray = self.playerBlockers[c]
for index = #blockerArray, 1, -1 do
if blockerArray[index]:occupiesCell(cell) then
table.remove(blockerArray, index)
end
end
-- switches
local switchArray = self.switches[c]
for index = #switchArray, 1, -1 do
@ -714,6 +749,12 @@ function Room:serialize(includePlayers)
end
end
for color, blockerArray in pairs(self.playerBlockers) do
for _, blocker in pairs(blockerArray) do
emit(blocker.saveClass or "player_blocker", color, blocker)
end
end
for color, switchArray in pairs(self.switches) do
for _, switch in ipairs(switchArray) do
emit("switch", color, switch)

925
rooms/blocker_test.sav Normal file
View file

@ -0,0 +1,925 @@
{
objects={
{
class="player",
color="red",
active=true,
x=6,
y=11
},
{
class="player",
color="green",
active=true,
x=6,
y=11
},
{
class="player",
color="blue",
active=true,
x=6,
y=11
},
{
class="static_wall",
color="red",
y=11,
x=11
},
{
class="static_wall",
color="red",
y=10,
x=11
},
{
class="static_wall",
color="red",
y=9,
x=11
},
{
class="static_wall",
color="red",
y=8,
x=11
},
{
class="static_wall",
color="red",
y=7,
x=11
},
{
class="static_wall",
color="red",
y=6,
x=11
},
{
class="static_wall",
color="red",
y=5,
x=11
},
{
class="static_wall",
color="red",
y=2,
x=11
},
{
class="static_wall",
color="red",
y=1,
x=11
},
{
class="static_wall",
color="red",
y=2,
x=11
},
{
class="static_wall",
color="red",
y=3,
x=11
},
{
class="static_wall",
color="red",
y=4,
x=11
},
{
class="static_wall",
color="red",
y=1,
x=10
},
{
class="static_wall",
color="red",
y=1,
x=9
},
{
class="static_wall",
color="red",
y=1,
x=8
},
{
class="static_wall",
color="red",
y=1,
x=7
},
{
class="static_wall",
color="red",
y=1,
x=6
},
{
class="static_wall",
color="red",
y=1,
x=5
},
{
class="static_wall",
color="red",
y=1,
x=4
},
{
class="static_wall",
color="red",
y=1,
x=3
},
{
class="static_wall",
color="red",
y=1,
x=2
},
{
class="static_wall",
color="red",
y=1,
x=1
},
{
class="static_wall",
color="red",
y=2,
x=1
},
{
class="static_wall",
color="red",
y=3,
x=1
},
{
class="static_wall",
color="red",
y=4,
x=1
},
{
class="static_wall",
color="red",
y=5,
x=1
},
{
class="static_wall",
color="red",
y=6,
x=1
},
{
class="static_wall",
color="red",
y=7,
x=1
},
{
class="static_wall",
color="red",
y=8,
x=1
},
{
class="static_wall",
color="red",
y=9,
x=1
},
{
class="static_wall",
color="red",
y=10,
x=1
},
{
class="static_wall",
color="red",
y=11,
x=1
},
{
class="static_wall",
color="red",
y=11,
x=2
},
{
class="static_wall",
color="red",
y=11,
x=3
},
{
class="static_wall",
color="red",
y=11,
x=4
},
{
class="static_wall",
color="red",
y=11,
x=5
},
{
class="static_wall",
color="red",
y=11,
x=7
},
{
class="static_wall",
color="red",
y=11,
x=8
},
{
class="static_wall",
color="red",
y=11,
x=9
},
{
class="static_wall",
color="red",
y=11,
x=10
},
{
class="static_wall",
color="red",
y=5,
x=7
},
{
class="static_wall",
color="red",
y=4,
x=7
},
{
class="static_wall",
color="red",
y=3,
x=7
},
{
class="static_wall",
color="red",
y=2,
x=7
},
{
class="static_wall",
color="red",
y=2,
x=5
},
{
class="static_wall",
color="red",
y=3,
x=5
},
{
class="static_wall",
color="red",
y=4,
x=5
},
{
class="static_wall",
color="red",
y=5,
x=5
},
{
class="static_wall",
color="green",
y=11,
x=11
},
{
class="static_wall",
color="green",
y=10,
x=11
},
{
class="static_wall",
color="green",
y=9,
x=11
},
{
class="static_wall",
color="green",
y=8,
x=11
},
{
class="static_wall",
color="green",
y=7,
x=11
},
{
class="static_wall",
color="green",
y=6,
x=11
},
{
class="static_wall",
color="green",
y=5,
x=11
},
{
class="static_wall",
color="green",
y=2,
x=11
},
{
class="static_wall",
color="green",
y=1,
x=11
},
{
class="static_wall",
color="green",
y=2,
x=11
},
{
class="static_wall",
color="green",
y=3,
x=11
},
{
class="static_wall",
color="green",
y=4,
x=11
},
{
class="static_wall",
color="green",
y=1,
x=10
},
{
class="static_wall",
color="green",
y=1,
x=9
},
{
class="static_wall",
color="green",
y=1,
x=8
},
{
class="static_wall",
color="green",
y=1,
x=7
},
{
class="static_wall",
color="green",
y=1,
x=6
},
{
class="static_wall",
color="green",
y=1,
x=5
},
{
class="static_wall",
color="green",
y=1,
x=4
},
{
class="static_wall",
color="green",
y=1,
x=3
},
{
class="static_wall",
color="green",
y=1,
x=2
},
{
class="static_wall",
color="green",
y=1,
x=1
},
{
class="static_wall",
color="green",
y=2,
x=1
},
{
class="static_wall",
color="green",
y=3,
x=1
},
{
class="static_wall",
color="green",
y=4,
x=1
},
{
class="static_wall",
color="green",
y=5,
x=1
},
{
class="static_wall",
color="green",
y=6,
x=1
},
{
class="static_wall",
color="green",
y=7,
x=1
},
{
class="static_wall",
color="green",
y=8,
x=1
},
{
class="static_wall",
color="green",
y=9,
x=1
},
{
class="static_wall",
color="green",
y=10,
x=1
},
{
class="static_wall",
color="green",
y=11,
x=1
},
{
class="static_wall",
color="green",
y=11,
x=2
},
{
class="static_wall",
color="green",
y=11,
x=3
},
{
class="static_wall",
color="green",
y=11,
x=4
},
{
class="static_wall",
color="green",
y=11,
x=5
},
{
class="static_wall",
color="green",
y=11,
x=7
},
{
class="static_wall",
color="green",
y=11,
x=8
},
{
class="static_wall",
color="green",
y=11,
x=9
},
{
class="static_wall",
color="green",
y=11,
x=10
},
{
class="static_wall",
color="green",
y=5,
x=7
},
{
class="static_wall",
color="green",
y=4,
x=7
},
{
class="static_wall",
color="green",
y=3,
x=7
},
{
class="static_wall",
color="green",
y=2,
x=7
},
{
class="static_wall",
color="green",
y=2,
x=5
},
{
class="static_wall",
color="green",
y=3,
x=5
},
{
class="static_wall",
color="green",
y=4,
x=5
},
{
class="static_wall",
color="green",
y=5,
x=5
},
{
class="static_wall",
color="blue",
y=11,
x=11
},
{
class="static_wall",
color="blue",
y=10,
x=11
},
{
class="static_wall",
color="blue",
y=9,
x=11
},
{
class="static_wall",
color="blue",
y=8,
x=11
},
{
class="static_wall",
color="blue",
y=7,
x=11
},
{
class="static_wall",
color="blue",
y=6,
x=11
},
{
class="static_wall",
color="blue",
y=5,
x=11
},
{
class="static_wall",
color="blue",
y=2,
x=11
},
{
class="static_wall",
color="blue",
y=1,
x=11
},
{
class="static_wall",
color="blue",
y=2,
x=11
},
{
class="static_wall",
color="blue",
y=3,
x=11
},
{
class="static_wall",
color="blue",
y=4,
x=11
},
{
class="static_wall",
color="blue",
y=1,
x=10
},
{
class="static_wall",
color="blue",
y=1,
x=9
},
{
class="static_wall",
color="blue",
y=1,
x=8
},
{
class="static_wall",
color="blue",
y=1,
x=7
},
{
class="static_wall",
color="blue",
y=1,
x=6
},
{
class="static_wall",
color="blue",
y=1,
x=5
},
{
class="static_wall",
color="blue",
y=1,
x=4
},
{
class="static_wall",
color="blue",
y=1,
x=3
},
{
class="static_wall",
color="blue",
y=1,
x=2
},
{
class="static_wall",
color="blue",
y=1,
x=1
},
{
class="static_wall",
color="blue",
y=2,
x=1
},
{
class="static_wall",
color="blue",
y=3,
x=1
},
{
class="static_wall",
color="blue",
y=4,
x=1
},
{
class="static_wall",
color="blue",
y=5,
x=1
},
{
class="static_wall",
color="blue",
y=6,
x=1
},
{
class="static_wall",
color="blue",
y=7,
x=1
},
{
class="static_wall",
color="blue",
y=8,
x=1
},
{
class="static_wall",
color="blue",
y=9,
x=1
},
{
class="static_wall",
color="blue",
y=10,
x=1
},
{
class="static_wall",
color="blue",
y=11,
x=1
},
{
class="static_wall",
color="blue",
y=11,
x=2
},
{
class="static_wall",
color="blue",
y=11,
x=3
},
{
class="static_wall",
color="blue",
y=11,
x=4
},
{
class="static_wall",
color="blue",
y=11,
x=5
},
{
class="static_wall",
color="blue",
y=11,
x=7
},
{
class="static_wall",
color="blue",
y=11,
x=8
},
{
class="static_wall",
color="blue",
y=11,
x=9
},
{
class="static_wall",
color="blue",
y=11,
x=10
},
{
class="static_wall",
color="blue",
y=5,
x=7
},
{
class="static_wall",
color="blue",
y=4,
x=7
},
{
class="static_wall",
color="blue",
y=3,
x=7
},
{
class="static_wall",
color="blue",
y=2,
x=7
},
{
class="static_wall",
color="blue",
y=2,
x=5
},
{
class="static_wall",
color="blue",
y=3,
x=5
},
{
class="static_wall",
color="blue",
y=4,
x=5
},
{
class="static_wall",
color="blue",
y=5,
x=5
},
{
class="player_blocker",
color="red",
y=5,
x=6
},
{
class="player_blocker",
color="green",
y=5,
x=6
},
{
class="player_blocker",
color="blue",
y=5,
x=6
},
{
class="switch",
color="red",
y=2,
x=6
},
{
class="switch",
color="green",
y=2,
x=6
},
{
class="switch",
color="blue",
y=2,
x=6
}
}
}

View file

@ -1,207 +1,212 @@
{
width=18,
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=13,
name="village_3",
x=13,
y=8
},
{
x=11,
name="village_4",
x=11,
y=8
},
{
x=11,
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
},
{
name="blocker_test",
x=7,
name="room_07_07",
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
},
{
name="room_08_07",
x=8,
y=7
}
},
lastRoom="jorge2"
width=18
}

View file

@ -361,9 +361,12 @@ end
local function roomWith(player, entities, width, height)
gridWidth, gridHeight = width or 8, height or 8
local matrix = {}
local blockerMatrix = {}
for y = 1, gridHeight do
matrix[y] = {}
blockerMatrix[y] = {}
for x = 1, gridWidth do matrix[y][x] = false end
for x = 1, gridWidth do blockerMatrix[y][x] = false end
end
for _, entity in ipairs(entities) do
for _, cell in ipairs(entity:getOccupiedCells()) do
@ -376,12 +379,61 @@ local function roomWith(player, entities, width, height)
colorsPlayerHas = {"red"},
player = {red = player},
collidableMatrices = {red = matrix},
playerBlockerMatrices = {red = blockerMatrix},
}, {__index = Room})
room.playerJoinCheck = function() end
room.tick = function() end
return room
end
test("Room keeps player-only blockers out of block collision", function()
loadRoomClass()
local oldAssets, oldLoadSprite = globalAssetProperties, loadSprite
local sprite = {getWidth = function() return 16 end, getHeight = function() return 16 end}
globalAssetProperties = {
player_blocker = {
class = "player_blocker",
sprites = {red = "player_blocker_r.png"},
cells = {red = {{x = 1, y = 1}}},
},
}
loadSprite = function() return sprite end
local room = setmetatable({
collideables = {red = {}, green = {}, blue = {}},
playerBlockers = {red = {}, green = {}, blue = {}},
}, {__index = Room})
local blocker = room:createEntity("player_blocker", "red", 4, 3)
assertTrue(blocker:blocksPlayer())
assertTrue(#room.playerBlockers.red == 1)
assertTrue(#room.collideables.red == 0, "blockers must not enter block collision")
globalAssetProperties, loadSprite = oldAssets, oldLoadSprite
end)
test("Room player-only blockers stop the player but do not stop a box", function()
loadRoomClass()
local player = movable(2, 3)
local box = movable(3, 3)
local blocker = movable(4, 3)
function blocker:blocksPlayer() return true end
local room = roomWith(player, {box}, 8, 8)
local blockerMatrix = {}
for y = 1, gridHeight do
blockerMatrix[y] = {}
for x = 1, gridWidth do blockerMatrix[y][x] = false end
end
blockerMatrix[3][4] = blocker
room.playerBlockerMatrices.red = blockerMatrix
room:movePlayer("right")
assertTrue(box.x == 4, "box should move onto the player-only blocker")
assertTrue(player.x == 3, "player should move while pushing the box through it")
room:movePlayer("right")
assertTrue(player.x == 3, "player should be stopped by the player-only blocker")
end)
test("Room applies the sticky-regular-player pull", function()
loadRoomClass()
local sticky = movable(2, 3, true)