chroma_solstice/room.lua

797 lines
22 KiB
Lua
Raw Normal View History

2015-03-11 20:11:29 -04:00
Room = class("Room")
2015-03-11 21:41:02 -04:00
require "libs/TSerial"
2015-03-11 21:08:15 -04:00
require "player"
2015-03-12 05:13:04 -04:00
require "artifact"
2015-03-14 05:36:02 -04:00
require "npc"
require "art"
require "assorted"
2015-03-15 05:26:47 -04:00
require "switch"
2015-03-15 07:08:35 -04:00
require "door"
2015-03-17 02:15:28 -04:00
require "dialogue_manager"
2015-03-25 01:38:53 -04:00
require "generic_moveable"
require "generic_unmoveable"
2015-03-11 20:11:29 -04:00
2015-03-11 21:08:15 -04:00
function Room:initialize(t)
2015-03-15 15:34:17 -04:00
self.player = {}
2015-03-15 05:26:47 -04:00
self.switches = { red = {}, green = {}, blue = {} }
2015-03-16 10:17:06 -04:00
self.doors = {}
2015-03-15 05:26:47 -04:00
self.collideables = { red = {}, green = {}, blue = {} }
2026-08-07 00:31:35 -04:00
self.npcs = {}
2015-03-17 01:59:14 -04:00
self.activeColors = {red = false, green = false, blue = false}
self.colorsPlayerHas = {}
if t.name then
print("loaded!!")
2026-08-07 01:57:48 -04:00
self:load(t.name, t.runtime) -- registers objects (and any saved players) from the .sav
2015-03-25 01:38:53 -04:00
print(t.name)
2026-08-07 00:31:35 -04:00
2015-03-25 01:38:53 -04:00
if t.player then
2026-08-07 00:31:35 -04:00
-- entering from another room: place the carried colors at the entry point
2015-03-25 01:38:53 -04:00
for _, color in ipairs(t.player.activeColors) do
2026-08-07 00:31:35 -04:00
if self.player[color] then
self.player[color].x, self.player[color].y = t.player.x, t.player.y
else
self:registerPlayer(Player:new(t.player.x, t.player.y, color), color)
end
2015-03-25 01:38:53 -04:00
self:setActiveColor(color, true)
end
2015-03-17 01:59:14 -04:00
end
2026-08-07 00:31:35 -04:00
2015-03-17 01:59:14 -04:00
self.name = t.name
else
self.colorsPlayerHas = {"red", "green", "blue"}
2015-03-12 03:04:09 -04:00
2015-03-17 01:59:14 -04:00
for _, color in ipairs(t.player.activeColors) do
self.player[color] = Player:new(t.player.x, t.player.y, color)
end
2015-03-15 15:34:17 -04:00
2015-03-17 01:59:14 -04:00
self.activeColors = {}
for _, color in ipairs(t.player.activeColors) do self.activeColors[color] = true end
2015-03-11 21:08:15 -04:00
2015-03-17 01:59:14 -04:00
-- for color, coords in pairs(t.player) do
-- self.player[color] = Player:new(coords.x, coords.y, color)
-- self.activeColors[color] = true
-- end
2015-03-25 01:38:53 -04:00
-- for i=1, 1 do
-- local x, y = randomPosition()
-- -- local door = Door:new(x, y, self.colorsPlayerHas)
-- -- table.insert(self.doors, door)
2015-03-11 20:11:29 -04:00
2015-03-25 01:38:53 -04:00
-- for color, active in pairs(self.activeColors) do
-- table.insert(self.collideables[color], Wall:new(x, y, color))
-- end
-- end
2015-03-12 05:13:04 -04:00
2015-03-25 01:38:53 -04:00
-- for i=0, 5 do
2015-03-17 01:59:14 -04:00
-- local x, y = randomPosition()
2015-03-25 01:38:53 -04:00
-- for color, active in pairs(self.activeColors) do
-- table.insert(self.collideables[color], Box:new(x, y, color))
-- end
2015-03-17 01:59:14 -04:00
-- end
2015-03-15 05:26:47 -04:00
2015-03-25 01:38:53 -04:00
for i = 0, 5 do
local x, y = randomPosition()
print(x, y)
local c = randomColor()
table.insert(self.switches[c], Switch:new(x, y, c))
end
2015-03-17 01:59:14 -04:00
for color, active in pairs(self.activeColors) do
local x, y = 6, 3
-- if active then self.collideables[color]["n: " .. x .. ", " .. y] = Ass:new(x, y, color, "animals/processed/", "me_rach") end
2015-03-14 05:36:02 -04:00
2015-03-17 01:59:14 -04:00
local x, y = 1, 1
2015-03-25 01:38:53 -04:00
-- if active then table.insert(self.collideables[color], Ass:new(x, y, color, "nature/", "palm_scaled_reduced")) end
2015-03-14 05:36:02 -04:00
2015-03-17 01:59:14 -04:00
-- local x, y = 7, 7
-- if active then self.collideables[color]["n: " .. x .. ", " .. y] = Ass:new(x, y, color, "food/processed/", "beer_mug") end
2015-03-14 05:36:02 -04:00
2015-03-17 01:59:14 -04:00
-- self.collideables[color]["fpp"] = Artifact:new(6, 6, color)
end
end
2015-03-14 05:36:02 -04:00
2015-03-17 01:59:14 -04:00
self.switchMatrices = {}
self.collidableMatrices = {} --{red = {{...}{...}{...}}, ...
self:createSwitchMatrix()
self:createCollideablesMatrix()
2015-03-11 20:11:29 -04:00
end
2015-03-11 21:41:02 -04:00
function Room:update(dt)
2015-03-15 15:34:17 -04:00
for color, player in pairs(self.player) do
player:update(dt)
end
2015-03-11 20:11:29 -04:00
end
2015-03-11 21:41:02 -04:00
function Room:draw()
2026-08-07 00:40:45 -04:00
-- 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
-- naturally draws its red sprite on the red pass, etc. No per-object logic.
local has = {}
for _, channel in ipairs(self.colorsPlayerHas) do
has[channel] = true
if self.player[channel] then self.player[channel]:draw(channel) end
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
for _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
2015-03-15 15:34:17 -04:00
end
2026-08-07 00:40:45 -04:00
-- npcs (old "sages") are markers for now: a "?" tinted to the player's channels
2026-08-07 00:31:35 -04:00
local cellW, cellH = width / gridWidth, height / gridHeight
2026-08-07 00:40:45 -04:00
love.graphics.setColor(has.red and 255 or 0, has.green and 255 or 0, has.blue and 255 or 0)
2026-08-07 00:31:35 -04:00
for _, npc in ipairs(self.npcs) do
love.graphics.print("?", (npc.x - 1) * cellW + cellW * 0.3, (npc.y - 1) * cellH + cellH * 0.1, 0, 2, 2)
end
2015-03-12 03:04:09 -04:00
end
function Room:createCollideablesMatrix()
for color, entities in pairs(self.collideables) do
self.collidableMatrices[color] = self:createEntityMatrix(entities)
end
2015-03-12 03:04:09 -04:00
end
function Room:createSwitchMatrix()
for color, switches in pairs(self.switches) do
self.switchMatrices[color] = self:createEntityMatrix(switches)
end
end
function Room:createEntityMatrix(entities)
local matrix = {}
2015-03-12 03:04:09 -04:00
--lets populate it w/ empty stuff first
for i = 1, gridWidth do
local row = {}
for i = 1, gridHeight do
table.insert(row, false)
2015-03-12 03:04:09 -04:00
end
table.insert(matrix, row)
2015-03-12 03:04:09 -04:00
end
2015-03-17 01:59:14 -04:00
--assumes that entities table is set up li_e {index = ent1, anotherIndex = ent2, ...}
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_
matrix[occupiedCells.y][occupiedCells.x] = entity
end
end
-- printMatrix(matrix)
2015-03-17 01:59:14 -04:00
return matrix
2015-03-12 03:04:09 -04:00
end
function Room:getCellInMatrix(matrix, cell)
return matrix[cell.y][cell.x]
2015-03-12 03:04:09 -04:00
end
function Room:movePlayer(key)
2015-03-15 15:34:17 -04:00
local moved = false
2015-03-16 08:47:08 -04:00
local previousPositions = {} --these are so we can perform the overlap chec_
local newPositions = {}
2015-03-15 15:34:17 -04:00
for _, color in pairs(self:getActiveColors()) do
local entitiesToPush, entitiesToPull = {}, {}
local pos = self.player[color]:getGridPos()
2015-03-16 08:47:08 -04:00
previousPositions[color] = pos
2015-03-15 15:34:17 -04:00
local x, y = pos.x, pos.y
local dir = {x = 0, y = 0}
local axis
if key == "up" then
dir.y, axis = -1, "y"
elseif key == "down" then
dir.y, axis = 1, "y"
elseif key == "left" then
dir.x, axis = -1, "x"
elseif key == "right" then
dir.x, axis = 1, "x"
end
2015-03-15 15:34:17 -04:00
local move = true
-- PUSHHHHHHHHHHHHHHHHHHHHHH---------------------
2015-03-12 03:04:09 -04:00
2015-03-15 15:34:17 -04:00
local initialTargetCell = {
x = pos.x + dir.x, y = pos.y + dir.y
} -- this is for reference for each color as we change the targetCells table
2015-03-12 03:04:09 -04:00
2015-03-16 07:43:02 -04:00
local initialTargetEntity
2015-03-17 01:59:14 -04:00
if self:isInBounds(initialTargetCell) then
initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
end
2015-03-16 07:43:02 -04:00
if initialTargetEntity then initialTargetEntity:bump() end
2015-03-16 07:39:54 -04:00
2015-03-15 15:34:17 -04:00
if not self:isInBounds(initialTargetCell) then
2015-03-16 07:39:54 -04:00
-- let's us move out of bounds to Change Rooms without pulling a bloc_
2015-03-15 15:34:17 -04:00
move = true
2015-03-16 07:43:02 -04:00
elseif initialTargetEntity and initialTargetEntity:canPassOver() then
2015-03-16 07:39:54 -04:00
--we should put this in the else statement actually
2015-03-15 15:34:17 -04:00
move = true
else
local targetCells
2015-03-15 05:26:47 -04:00
targetCells = {initialTargetCell} -- ...we start with our attempted movement...
local doneSearching
while not doneSearching do
nextTargetCells = {} -- ...then we create this array to save our next group of cells
local emptyCells = {}
for _, cell in pairs(targetCells) do
if self:isInBounds(cell) then
local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
if entity then
--something is here!!
2015-03-15 15:34:17 -04:00
if not entitiesToPush[entity] then --if the entity hasnt been found already
2015-03-15 05:26:47 -04:00
if entity:canMove() then
local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
2015-03-15 15:34:17 -04:00
entitiesToPush[entity] = entity
2015-03-15 05:26:47 -04:00
for _, projectedCell in ipairs(projectedCells) do
table.insert(nextTargetCells, projectedCell)
end
else
move = false
doneSearching = true
end
end
2015-03-15 05:26:47 -04:00
else
--there's nothin there!!
table.insert(emptyCells, cell)
end
2015-03-15 05:26:47 -04:00
else
--we are out of bounds now
move = false
doneSearching = true
end
2015-03-15 05:26:47 -04:00
end
--if every projected cell is empty then we can do the move
if #emptyCells == #targetCells then
move = true
doneSearching = true
end
2015-03-15 05:26:47 -04:00
targetCells = nextTargetCells
end
2015-03-15 05:26:47 -04:00
--let's get out of this loop b/c if we can't move one color we can't move at all
end
2015-03-15 15:34:17 -04:00
---PULL CHEC
2015-03-15 15:34:17 -04:00
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
2015-03-15 15:34:17 -04:00
local targetCells
local pull
--for each color...
targetCells = {initialTargetCell} -- ...we start with our attempted movement...
local doneSearching
while not doneSearching do
nextTargetCells = {} -- ...then we create this array to save our next group of cells
local emptyCells = {}
for _, cell in pairs(targetCells) do
if self:isInBounds(cell) then
local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
if entity then
--something is here!!
2015-03-15 15:34:17 -04:00
if not entitiesToPull[entity] then --if the entity hasnt been found already
if entity:canMove() then
local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
2015-03-15 15:34:17 -04:00
entitiesToPull[entity] = entity
for _, projectedCell in ipairs(projectedCells) do
table.insert(nextTargetCells, projectedCell)
end
else
pull = false
doneSearching = true
end
end
else
--there's nothin there!!
table.insert(emptyCells, cell)
end
else
--we are out of bounds now
doneSearching = true
end
end
--if every projected cell is empty then we can do the move
if #emptyCells == #targetCells then
pull = true
doneSearching = true
end
targetCells = nextTargetCells
end
2015-03-15 15:34:17 -04:00
if move then
for _, entity in pairs(entitiesToPush) do
entity:move(axis, dir[axis])
end
if pull == true then
for _, entity in pairs(entitiesToPull) do
entity:move(axis, dir[axis])
end
end
2015-03-15 15:34:17 -04:00
self.player[color]:move(axis, dir[axis])
moved = true
end
2015-03-16 08:47:08 -04:00
newPositions[color] = self.player[color]:getGridPos()
2015-03-15 15:34:17 -04:00
end
if moved then
2015-03-16 08:47:08 -04:00
self:playerJoinCheck(previousPositions, newPositions)
self:tick()
end
end
function Room:switchPlayerColor(color)
2015-03-15 15:34:17 -04:00
local canSwitch
2015-03-12 03:04:09 -04:00
2015-03-15 15:34:17 -04:00
if self:onlyColorIsActive(color) then
canSwitch = false
else
for _, c in pairs(self:getPossibleColors()) do
if c == color then
self:setActiveColor(c, true)
else
self:setActiveColor(c, false)
end
end
end
2015-03-15 15:34:17 -04:00
self.player[color]:playSwitchSound(canSwitch)
end
function Room:setActiveColor(color, bool)
self.activeColors[color] = bool
2015-03-11 21:08:15 -04:00
end
2015-03-15 15:34:17 -04:00
function Room:getActiveColors()
local colors = {}
for color, active in pairs(self.activeColors) do
if active then
table.insert(colors, color)
end
end
return colors
end
2015-03-16 08:47:08 -04:00
function Room:colorIsActive(color)
if self.activeColors[color] then
return true
end
return false
end
2015-03-15 15:34:17 -04:00
function Room:getInactiveColors()
2026-08-07 00:31:35 -04:00
-- only colors the player actually has a copy of but that aren't active right
-- now (a left-behind copy). Colors the player hasn't acquired have no copy to
-- merge with, so they must not appear here.
2015-03-15 15:34:17 -04:00
local colors = {}
2026-08-07 00:31:35 -04:00
for _, color in ipairs(self.colorsPlayerHas) do
if not self.activeColors[color] then
2015-03-15 15:34:17 -04:00
table.insert(colors, color)
end
end
return colors
end
function Room:getPossibleColors()
local colors = {}
for _, color in ipairs(self.colorsPlayerHas) do
table.insert(colors, color)
end
return colors
end
function Room:onlyColorIsActive(color)
local activeColors = self:getActiveColors()
if #activeColors == 1 and activeColors[1] == color then
return true
else
return false
end
end
2015-03-11 21:08:15 -04:00
function Room:tick()
2015-03-12 03:04:09 -04:00
for color, entityArray in pairs(self.collideables) do
for _, entity in pairs(entityArray) do
entity:tick()
end
end
self:createCollideablesMatrix()
self:createSwitchMatrix()
2015-03-15 07:08:35 -04:00
local setDoorColors = self:switchCheck()
for _, door in pairs(self.doors) do
door:setColors(setDoorColors)
end
2015-03-11 21:08:15 -04:00
self:nextRoomCheck()
end
2015-03-16 08:47:08 -04:00
function Room:playerJoinCheck(previousPositions, newPositions)
local activate = {}
local deactivate = {}
local activeColors = self:getActiveColors()
for _, color in ipairs(activeColors) do
if #activeColors > 1 then
if Entity.positionsAreEqual(previousPositions[color], newPositions[color]) then
table.insert(deactivate, color)
end
end
end
2015-03-15 15:34:17 -04:00
for _, inactiveColor in ipairs(self:getInactiveColors()) do
local overlapping = 0
local activeColors = self:getActiveColors()
local inactivePos = self:getPlayerPos(inactiveColor)
for _, activeColor in ipairs(activeColors) do
local activePos = self:getPlayerPos(activeColor)
if activePos.x == inactivePos.x and activePos.y == inactivePos.y then
overlapping = overlapping + 1
end
end
if overlapping == #activeColors then
2015-03-16 08:47:08 -04:00
table.insert(activate, inactiveColor)
2015-03-15 15:34:17 -04:00
end
end
2015-03-16 08:47:08 -04:00
for _, color in ipairs(activate) do
2015-03-15 15:34:17 -04:00
self:setActiveColor(color, true)
end
2015-03-16 08:47:08 -04:00
for _, color in ipairs(deactivate) do
self:setActiveColor(color, false)
end
2015-03-15 15:34:17 -04:00
end
function Room:getPlayerPos(color)
return self.player[color]:getGridPos()
end
2015-03-16 10:17:06 -04:00
function Room:getPlayerObject(color)
return self.player[color]
end
function Room:getDoors()
return self.doors
end
function Room:getCollideables()
return self.collideables
end
function Room:getSwitches()
return self.switches
end
2015-03-15 07:08:35 -04:00
function Room:switchCheck()
local doorsToUnlock = {}
for color, switchArray in pairs(self.switches) do
local onSwitches = {}
for _, switch in pairs(switchArray) do
local targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
2026-08-07 02:01:39 -04:00
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.
if targetCell or (player and player:occupiesCell(switch:getGridPos())) then
--switch is on
switch:activate()
table.insert(onSwitches, switch) --doesn't rly matter what we put here as long as it's true-y
2015-03-15 13:54:47 -04:00
else
switch:deactivate()
end
end
2015-03-15 07:08:35 -04:00
if #onSwitches == #switchArray then doorsToUnlock[color] = true else doorsToUnlock[color] = false end
end
2015-03-15 07:08:35 -04:00
return doorsToUnlock
end
function Room:isInBounds(cell)
if cell.x > 0 and cell.x <= gridWidth and
cell.y > 0 and cell.y <= gridHeight then
return true
end
return false
end
2015-03-11 21:08:15 -04:00
function Room:nextRoomCheck()
2015-03-15 15:34:17 -04:00
for _, color in ipairs(self:getActiveColors()) do
2026-08-07 00:31:35 -04:00
local player = self.player[color]
if player and not self:isInBounds(player:getGridPos()) then
nextRoom(player:getGridPos())
2015-03-15 15:34:17 -04:00
return
end
2015-03-15 05:26:47 -04:00
end
2015-03-11 21:08:15 -04:00
end
2015-03-17 01:59:14 -04:00
2026-08-07 00:56:33 -04:00
-- 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"); anything without a .meta defaults to immoveable. npc is the one
-- keyword handled directly since it has no art.
2015-03-17 01:59:14 -04:00
function Room:createEntity(class, color, x, y)
2026-08-07 00:56:33 -04:00
if class == "npc" then
self:registerNpc(x, y)
return
end
local props = globalAssetProperties and globalAssetProperties[class]
local behavior = props and props.class or "immoveable"
2026-08-07 00:31:35 -04:00
local entity
2026-08-07 00:56:33 -04:00
if behavior == "door" then
2026-08-07 01:57:48 -04:00
-- Old migrated rooms stored a colourless shared door. Expand that legacy
-- record into ordinary colour-layer door objects on load.
if not color then
for _, doorColor in ipairs({"red", "green", "blue"}) do
if props.sprites[doorColor] then self:createEntity(class, doorColor, x, y) end
end
return
end
entity = Door:new(x, y, color)
self:registerDoor(entity, color)
2026-08-07 00:56:33 -04:00
elseif behavior == "switch" then
2026-08-07 00:31:35 -04:00
entity = Switch:new(x, y, color)
self:registerSwitch(entity, color)
2026-08-07 00:56:33 -04:00
elseif behavior == "player" then
2026-08-07 00:31:35 -04:00
entity = Player:new(x, y, color)
self:registerPlayer(entity, color)
self:setActiveColor(color, true)
2026-08-07 00:56:33 -04:00
elseif props and props.sprites and props.sprites[color] then
2026-08-07 01:05:08 -04:00
local sprite = loadSprite(props.sprites[color])
2026-08-07 00:56:33 -04:00
if behavior == "moveable" then
entity = GenericMoveable:new(x, y, color, sprite, class)
else
entity = GenericUnmoveable:new(x, y, color, sprite)
2026-08-07 00:31:35 -04:00
end
2026-08-07 00:56:33 -04:00
self:registerCollidable(entity, {color})
2015-03-17 01:59:14 -04:00
end
2026-08-07 00:56:33 -04:00
2026-08-07 00:31:35 -04:00
if entity then entity.saveClass = class end
return entity
2015-03-17 01:59:14 -04:00
end
function Room:registerCollidable(entityPointer, colorArray)
if type(colorArray) ~= "table" then colorArray = {colorArray} end
for _, color in ipairs(colorArray) do
table.insert(self.collideables[color], entityPointer)
end
end
2026-08-07 01:57:48 -04:00
function Room:registerDoor(entityPointer, color)
2015-03-17 01:59:14 -04:00
table.insert(self.doors, entityPointer)
2026-08-07 01:57:48 -04:00
self:registerCollidable(entityPointer, color)
2015-03-17 01:59:14 -04:00
end
function Room:registerSwitch(entityPointer, color)
table.insert(self.switches[color], entityPointer)
end
function Room:registerPlayer(entityPointer, color)
table.insert(self.colorsPlayerHas, color)
self.player[color] = entityPointer
end
2026-08-07 00:31:35 -04:00
function Room:registerNpc(x, y)
table.insert(self.npcs, { x = x, y = y })
end
2015-03-25 01:38:53 -04:00
function Room:getCellFromMousePos(mouseX, mouseY)
local cell = {}
cell.x = math.ceil(mouseX * gridWidth / width)
cell.y = math.ceil(mouseY * gridHeight / height)
return cell
end
2026-08-06 23:54:49 -04:00
function Room:attemptDelete(mouseX, mouseY, color)
2015-03-25 01:38:53 -04:00
local cell = self:getCellFromMousePos(mouseX, mouseY)
2026-08-06 23:54:49 -04:00
-- "white" (or nil) deletes every channel; otherwise just the picked one
local colors
if not color or color == "white" then
colors = {"red", "green", "blue"}
else
colors = {color}
2015-03-25 01:38:53 -04:00
end
2026-08-06 23:54:49 -04:00
2026-08-07 01:57:48 -04:00
local removedEntities = {}
2026-08-06 23:54:49 -04:00
for _, c in ipairs(colors) do
-- player
if self.player[c] and self.player[c]:occupiesCell(cell) then
self.player[c] = nil
2026-08-07 00:31:35 -04:00
self:setActiveColor(c, false)
for i = #self.colorsPlayerHas, 1, -1 do
if self.colorsPlayerHas[i] == c then table.remove(self.colorsPlayerHas, i) end
end
2026-08-06 23:54:49 -04:00
end
-- collidables (iterate backwards so table.remove is safe)
local entityArray = self.collideables[c]
for index = #entityArray, 1, -1 do
if entityArray[index]:occupiesCell(cell) then
2026-08-07 01:57:48 -04:00
removedEntities[entityArray[index]] = true
2026-08-06 23:54:49 -04:00
table.remove(entityArray, index)
2015-03-25 01:38:53 -04:00
end
end
2026-08-06 23:54:49 -04:00
-- switches
local switchArray = self.switches[c]
for index = #switchArray, 1, -1 do
if switchArray[index]:occupiesCell(cell) then
table.remove(switchArray, index)
2015-03-25 01:38:53 -04:00
end
end
end
2026-08-07 01:57:48 -04:00
-- Behaviour registries hold references to the same objects as the colour
-- layers. Clean references by identity; no class gets bespoke delete rules.
for index = #self.doors, 1, -1 do
if removedEntities[self.doors[index]] then table.remove(self.doors, index) end
end
2015-03-25 01:38:53 -04:00
self:tick()
end
2026-08-06 23:54:49 -04:00
function Room:attemptPlace(name, props, mouseX, mouseY, color)
if not props or not props.sprites then return end
local cell = self:getCellFromMousePos(mouseX, mouseY)
2015-03-25 01:38:53 -04:00
2026-08-06 23:54:49 -04:00
-- "white" (or nil) means every channel; otherwise just the picked channel
local colors
if not color or color == "white" then
colors = {"red", "green", "blue"}
else
colors = {color}
end
2026-08-07 00:31:35 -04:00
-- one entity per placed channel, via the same path level-loading uses; the
-- additive blend combines the channels into full color
2026-08-06 23:54:49 -04:00
for _, c in ipairs(colors) do
2026-08-07 00:31:35 -04:00
if props.sprites[c] then
self:createEntity(name, c, cell.x, cell.y)
2026-08-06 23:54:49 -04:00
end
end
self:tick()
2015-03-25 01:38:53 -04:00
end
2026-08-07 01:57:48 -04:00
function Room:load(name, preferRuntime)
-- Editor/startup loads must use the project source level. Runtime saves are
-- only consulted for an in-progress room-to-room game transition, otherwise
-- an old runtime autosave can hide an editor Save after restart.
local room
if preferRuntime then room = love.filesystem.read("rooms/" .. name .. ".sav") end
if not room then room = readFromSource("rooms/" .. name .. ".sav") end
2026-08-06 23:54:49 -04:00
if not room then
-- no save yet: start with a blank room to build in the editor
print("no save for '" .. name .. "', starting blank")
return
end
2015-03-17 01:59:14 -04:00
self:deserialize(TSerial.unpack(room))
end
function Room:deserialize(saveTable)
2026-08-07 00:31:35 -04:00
for _, obj in ipairs(saveTable.objects or {}) do
self:createEntity(obj.class, obj.color, obj.x, obj.y)
2015-03-17 01:59:14 -04:00
end
end
function Room:save()
2015-03-17 01:59:14 -04:00
local name = self.name or "test"
local room = TSerial.pack(self:serialize(), nil, true)
2026-08-07 01:39:47 -04:00
local success = love.filesystem.write("rooms/" .. name .. ".sav", room)
2015-03-17 01:59:14 -04:00
if not success then error "great job u fool" end
end
2026-08-07 01:39:47 -04:00
-- This is intentionally separate from Room:save. It is the editor's explicit
-- "write this level into the project" action; gameplay autosaves must never
-- call it.
2026-08-07 03:22:28 -04:00
function Room:saveToSource(name, savePlayerPositions, playerSourceName)
2026-08-07 01:39:47 -04:00
name = name or self.name or "test"
2026-08-07 03:22:28 -04:00
local saveTable = self:serialize(savePlayerPositions ~= false)
if savePlayerPositions == false then
-- A normal editor save keeps the authored spawn positions from the
-- existing source level. During a rename that source is still oldName.
local sourceName = playerSourceName or name
local existing = readFromSource("rooms/" .. sourceName .. ".sav")
local keptPlayer = false
if existing then
local previous = TSerial.unpack(existing)
for _, object in ipairs(previous.objects or {}) do
if object.class == "player" then
table.insert(saveTable.objects, object)
keptPlayer = true
end
end
end
-- A newly named blank level has no prior source spawn to retain.
if not keptPlayer then
for color, player in pairs(self.player) do
local pos = player:getGridPos()
table.insert(saveTable.objects, { class = "player", color = color, x = pos.x, y = pos.y })
end
end
end
local room = TSerial.pack(saveTable, nil, true)
2026-08-07 01:39:47 -04:00
return writeToSource("rooms/" .. name .. ".sav", room)
end
2026-08-07 03:22:28 -04:00
function Room:serialize(includePlayers)
2026-08-07 00:31:35 -04:00
local objects = {}
2015-03-16 10:17:06 -04:00
2026-08-07 00:31:35 -04:00
local function emit(class, color, entity)
local pos = entity:getGridPos()
table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y })
2015-03-16 10:17:06 -04:00
end
2026-08-07 03:22:28 -04:00
if includePlayers ~= false then
for color, player in pairs(self.player) do
emit("player", color, player)
end
2015-03-16 10:17:06 -04:00
end
2026-08-07 00:31:35 -04:00
for color, collideableArray in pairs(self.collideables) do
2015-03-16 10:17:06 -04:00
for _, collideable in pairs(collideableArray) do
2026-08-07 01:57:48 -04:00
emit(collideable.saveClass or "unmoveable", color, collideable)
2015-03-16 10:17:06 -04:00
end
end
2026-08-07 00:31:35 -04:00
for color, switchArray in pairs(self.switches) do
2015-03-16 10:17:06 -04:00
for _, switch in ipairs(switchArray) do
2026-08-07 00:31:35 -04:00
emit("switch", color, switch)
2015-03-16 10:17:06 -04:00
end
end
2026-08-07 00:31:35 -04:00
for _, npc in ipairs(self.npcs) do
table.insert(objects, { class = "npc", x = npc.x, y = npc.y })
end
return { objects = objects }
end
2015-03-15 05:26:47 -04:00
function nextRoom(pos)
local exit
local newPos = {x = pos.x, y = pos.y}
if pos.x > gridWidth then
exit = "right"
newPos.x = 1
elseif pos.x < 1 then
exit = "left"
newPos.x = gridWidth
elseif pos.y > gridHeight then
exit = "bottom"
newPos.y = 1
elseif pos.y < 1 then
exit = "top"
newPos.y = gridHeight
end
2015-03-12 03:04:09 -04:00
2026-08-07 01:39:47 -04:00
local currentName = currentRoom.name
local nextName = gameWorld and currentName and gameWorld:getNeighbor(currentName, exit)
currentRoom:save()
2026-08-07 01:39:47 -04:00
-- Missing map neighbours intentionally wrap back into the room just exited.
-- A placed neighbour gets the same carried player state at its opposite edge.
currentRoom = Room:new{
name = nextName or currentName,
2026-08-07 01:57:48 -04:00
runtime = true,
2015-03-15 05:26:47 -04:00
player = {
2015-03-15 15:34:17 -04:00
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
2015-03-15 05:26:47 -04:00
}
}
2026-08-07 03:22:28 -04:00
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
2026-08-07 01:39:47 -04:00
if currentRoom.name then setEditorRoomName(currentRoom.name) end
2015-03-17 01:59:14 -04:00
end