Room = class("Room") require "libs/TSerial" require "player" require "box" require "wall" require "artifact" require "npc" require "art" require "assorted" require "switch" require "door" require "dialogue_manager" require "generic_moveable" require "generic_unmoveable" -- cache loaded images so repeated placing/loading doesn't re-decode PNGs local placeImageCache = {} local function loadPlaceImage(path) if not placeImageCache[path] then placeImageCache[path] = love.graphics.newImage(path) end return placeImageCache[path] end function Room:initialize(t) self.player = {} self.switches = { red = {}, green = {}, blue = {} } self.doors = {} self.collideables = { red = {}, green = {}, blue = {} } self.npcs = {} self.activeColors = {red = false, green = false, blue = false} self.colorsPlayerHas = {} if t.name then print("loaded!!") self:load(t.name) -- registers objects (and any saved players) from the .sav print(t.name) if t.player then -- entering from another room: place the carried colors at the entry point for _, color in ipairs(t.player.activeColors) do 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 self:setActiveColor(color, true) end elseif not next(self.player) then -- fresh load of a room with no saved player: default red spawn so it's -- playable (the player starts with only red) self:registerPlayer(Player:new(1, 1, "red"), "red") self:setActiveColor("red", true) end self.name = t.name else self.colorsPlayerHas = {"red", "green", "blue"} for _, color in ipairs(t.player.activeColors) do self.player[color] = Player:new(t.player.x, t.player.y, color) end self.activeColors = {} for _, color in ipairs(t.player.activeColors) do self.activeColors[color] = true end -- for color, coords in pairs(t.player) do -- self.player[color] = Player:new(coords.x, coords.y, color) -- self.activeColors[color] = true -- end -- for i=1, 1 do -- local x, y = randomPosition() -- -- local door = Door:new(x, y, self.colorsPlayerHas) -- -- table.insert(self.doors, door) -- for color, active in pairs(self.activeColors) do -- table.insert(self.collideables[color], Wall:new(x, y, color)) -- end -- end -- for i=0, 5 do -- local x, y = randomPosition() -- for color, active in pairs(self.activeColors) do -- table.insert(self.collideables[color], Box:new(x, y, color)) -- end -- end 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 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 local x, y = 1, 1 -- if active then table.insert(self.collideables[color], Ass:new(x, y, color, "nature/", "palm_scaled_reduced")) end -- local x, y = 7, 7 -- if active then self.collideables[color]["n: " .. x .. ", " .. y] = Ass:new(x, y, color, "food/processed/", "beer_mug") end -- self.collideables[color]["fpp"] = Artifact:new(6, 6, color) end end self.switchMatrices = {} self.collidableMatrices = {} --{red = {{...}{...}{...}}, ... self:createSwitchMatrix() self:createCollideablesMatrix() end function Room:update(dt) for color, player in pairs(self.player) do player:update(dt) end end function Room:draw() -- 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 end -- npcs (old "sages") are markers for now: a "?" tinted to the player's channels local cellW, cellH = width / gridWidth, height / gridHeight love.graphics.setColor(has.red and 255 or 0, has.green and 255 or 0, has.blue and 255 or 0) 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 end function Room:createCollideablesMatrix() for color, entities in pairs(self.collideables) do self.collidableMatrices[color] = self:createEntityMatrix(entities) end 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 = {} --lets populate it w/ empty stuff first for i = 1, gridWidth do local row = {} for i = 1, gridHeight do table.insert(row, false) end table.insert(matrix, row) end --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) return matrix end function Room:getCellInMatrix(matrix, cell) return matrix[cell.y][cell.x] end function Room:movePlayer(key) local moved = false local previousPositions = {} --these are so we can perform the overlap chec_ local newPositions = {} for _, color in pairs(self:getActiveColors()) do local entitiesToPush, entitiesToPull = {}, {} local pos = self.player[color]:getGridPos() previousPositions[color] = pos 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 local move = true -- PUSHHHHHHHHHHHHHHHHHHHHHH--------------------- 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 local initialTargetEntity if self:isInBounds(initialTargetCell) then initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell) end if initialTargetEntity then initialTargetEntity:bump() end if not self:isInBounds(initialTargetCell) then -- let's us move out of bounds to Change Rooms without pulling a bloc_ move = true elseif initialTargetEntity and initialTargetEntity:canPassOver() then --we should put this in the else statement actually move = true else local targetCells 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!! if not entitiesToPush[entity] then --if the entity hasnt been found already if entity:canMove() then local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here entitiesToPush[entity] = entity for _, projectedCell in ipairs(projectedCells) do table.insert(nextTargetCells, projectedCell) end else move = false doneSearching = true end end else --there's nothin there!! table.insert(emptyCells, cell) end else --we are out of bounds now move = false doneSearching = true end end --if every projected cell is empty then we can do the move if #emptyCells == #targetCells then move = true doneSearching = true end targetCells = nextTargetCells end --let's get out of this loop b/c if we can't move one color we can't move at all end ---PULL CHEC local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y} 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!! 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 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 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 self.player[color]:move(axis, dir[axis]) moved = true end newPositions[color] = self.player[color]:getGridPos() end if moved then self:playerJoinCheck(previousPositions, newPositions) self:tick() end end function Room:switchPlayerColor(color) local canSwitch 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 self.player[color]:playSwitchSound(canSwitch) end function Room:setActiveColor(color, bool) self.activeColors[color] = bool end 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 function Room:colorIsActive(color) if self.activeColors[color] then return true end return false end function Room:getInactiveColors() -- 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. local colors = {} for _, color in ipairs(self.colorsPlayerHas) do if not self.activeColors[color] then 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 function Room:tick() for color, entityArray in pairs(self.collideables) do for _, entity in pairs(entityArray) do entity:tick() end end self:createCollideablesMatrix() self:createSwitchMatrix() local setDoorColors = self:switchCheck() for _, door in pairs(self.doors) do door:setColors(setDoorColors) end self:nextRoomCheck() end 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 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 table.insert(activate, inactiveColor) end end for _, color in ipairs(activate) do self:setActiveColor(color, true) end for _, color in ipairs(deactivate) do self:setActiveColor(color, false) end end function Room:getPlayerPos(color) return self.player[color]:getGridPos() end 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 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()) if targetCell 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 else switch:deactivate() end end if #onSwitches == #switchArray then doorsToUnlock[color] = true else doorsToUnlock[color] = false end end 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 function Room:nextRoomCheck() for _, color in ipairs(self:getActiveColors()) do local player = self.player[color] if player and not self:isInBounds(player:getGridPos()) then nextRoom(player:getGridPos()) return end end 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"); 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 self:registerNpc(x, y) return end local props = globalAssetProperties and globalAssetProperties[class] local behavior = props and props.class or "immoveable" local entity if behavior == "door" then entity = Door:new(x, y) self:registerDoor(entity) elseif behavior == "switch" then entity = Switch:new(x, y, color) self:registerSwitch(entity, color) elseif behavior == "player" then entity = Player:new(x, y, color) self:registerPlayer(entity, color) self:setActiveColor(color, true) elseif props and props.sprites and props.sprites[color] then local sprite = loadPlaceImage(props.sprites[color]) if behavior == "moveable" then entity = GenericMoveable:new(x, y, color, sprite, class) else entity = GenericUnmoveable:new(x, y, color, sprite) end self:registerCollidable(entity, {color}) end if entity then entity.saveClass = class end return entity 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 function Room:registerDoor(entityPointer) table.insert(self.doors, entityPointer) self:registerCollidable(entityPointer, {"red", "green", "blue"}) 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 function Room:registerNpc(x, y) table.insert(self.npcs, { x = x, y = y }) end function Room:getCellFromMousePos(mouseX, mouseY) local cell = {} cell.x = math.ceil(mouseX * gridWidth / width) cell.y = math.ceil(mouseY * gridHeight / height) return cell end function Room:attemptDelete(mouseX, mouseY, color) local cell = self:getCellFromMousePos(mouseX, mouseY) -- "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} end for _, c in ipairs(colors) do -- player if self.player[c] and self.player[c]:occupiesCell(cell) then self.player[c] = nil self:setActiveColor(c, false) for i = #self.colorsPlayerHas, 1, -1 do if self.colorsPlayerHas[i] == c then table.remove(self.colorsPlayerHas, i) end end 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 table.remove(entityArray, index) end end -- switches local switchArray = self.switches[c] for index = #switchArray, 1, -1 do if switchArray[index]:occupiesCell(cell) then table.remove(switchArray, index) end end end self:tick() end function Room:attemptPlace(name, props, mouseX, mouseY, color) if not props or not props.sprites then return end local cell = self:getCellFromMousePos(mouseX, mouseY) -- "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 -- one entity per placed channel, via the same path level-loading uses; the -- additive blend combines the channels into full color for _, c in ipairs(colors) do if props.sprites[c] then self:createEntity(name, c, cell.x, cell.y) end end self:tick() end function Room:load(name) local room = readFromSource("rooms/" .. name .. ".sav") 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 self:deserialize(TSerial.unpack(room)) end function Room:deserialize(saveTable) for _, obj in ipairs(saveTable.objects or {}) do self:createEntity(obj.class, obj.color, obj.x, obj.y) end end function Room:save() local name = self.name or "test" local room = TSerial.pack(self:serialize(), nil, true) local success = writeToSource("rooms/" .. name .. ".sav", room) if not success then error "great job u fool" end end function Room:serialize() local objects = {} local function emit(class, color, entity) local pos = entity:getGridPos() table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y }) end for color, player in pairs(self.player) do emit("player", color, player) end -- collideables hold walls/static_walls/generics on one color each, plus -- doors registered across all three colors -> dedupe doors, emit once local seenDoor = {} for color, collideableArray in pairs(self.collideables) do for _, collideable in pairs(collideableArray) do if collideable:isInstanceOf(Door) then if not seenDoor[collideable] then seenDoor[collideable] = true emit("door", nil, collideable) end else emit(collideable.saveClass or "unmoveable", color, collideable) end end end for color, switchArray in pairs(self.switches) do for _, switch in ipairs(switchArray) do emit("switch", color, switch) end end for _, npc in ipairs(self.npcs) do table.insert(objects, { class = "npc", x = npc.x, y = npc.y }) end return { objects = objects } end 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 currentRoom:save() currentRoom = Room:new{ -- name = "test", player = { x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors() } } end