Add world-level movement and seam rendering
This commit is contained in:
parent
a60f46d585
commit
1965e548b4
3 changed files with 386 additions and 27 deletions
21
main.lua
21
main.lua
|
|
@ -35,6 +35,9 @@ function love.load()
|
||||||
_initAccessibility()
|
_initAccessibility()
|
||||||
gameWorld = World:new()
|
gameWorld = World:new()
|
||||||
_initLevelEditor()
|
_initLevelEditor()
|
||||||
|
-- getAllAssets runs in _initLevelEditor; deserialize the world only after
|
||||||
|
-- its asset metadata exists, otherwise every non-player object is skipped.
|
||||||
|
gameWorld:loadLevels()
|
||||||
|
|
||||||
_initCanvases()
|
_initCanvases()
|
||||||
|
|
||||||
|
|
@ -43,10 +46,14 @@ function love.load()
|
||||||
-- Resume the game: prefer the runtime autosave so the last saved position is
|
-- Resume the game: prefer the runtime autosave so the last saved position is
|
||||||
-- restored, falling back to the authored source level. Editor room-opens go
|
-- restored, falling back to the authored source level. Editor room-opens go
|
||||||
-- through their own (source-only) load path, so this only affects resume.
|
-- through their own (source-only) load path, so this only affects resume.
|
||||||
currentRoom = Room:new{
|
local initialRoom = gameWorld.lastRoom or "start"
|
||||||
name = gameWorld.lastRoom or "start",
|
-- Preserve the old resume behavior for the active room while the rest of the
|
||||||
runtime = true
|
-- world comes from the authored level files.
|
||||||
}
|
currentRoom = Room:new{name = initialRoom, runtime = true}
|
||||||
|
if gameWorld.levels[initialRoom] then
|
||||||
|
gameWorld.levels[initialRoom] = currentRoom
|
||||||
|
gameWorld:rebuildCollision()
|
||||||
|
end
|
||||||
gameWorld:setLastRoom(currentRoom.name)
|
gameWorld:setLastRoom(currentRoom.name)
|
||||||
setEditorRoomName(currentRoom.name)
|
setEditorRoomName(currentRoom.name)
|
||||||
|
|
||||||
|
|
@ -69,7 +76,7 @@ function love.update(dt)
|
||||||
Input:stopMoveRepeat()
|
Input:stopMoveRepeat()
|
||||||
else
|
else
|
||||||
local repeatedMove = Input:handler(dt)
|
local repeatedMove = Input:handler(dt)
|
||||||
if repeatedMove and not Dialogue.isOpen() then currentRoom:movePlayer(repeatedMove) end
|
if repeatedMove and not Dialogue.isOpen() then gameWorld:movePlayer(currentRoom, repeatedMove) end
|
||||||
end
|
end
|
||||||
currentRoom:update(dt)
|
currentRoom:update(dt)
|
||||||
|
|
||||||
|
|
@ -164,10 +171,10 @@ function love.keypressed(key)
|
||||||
|
|
||||||
if editorView ~= "world" and Input.isPlayerMove(key) then
|
if editorView ~= "world" and Input.isPlayerMove(key) then
|
||||||
Input:beginMove(key)
|
Input:beginMove(key)
|
||||||
currentRoom:movePlayer(key)
|
gameWorld:movePlayer(currentRoom, key)
|
||||||
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
|
elseif editorView ~= "world" and Input.isPlayerSwitch(key) then
|
||||||
local c = Input.playerColor(key)
|
local c = Input.playerColor(key)
|
||||||
currentRoom:switchPlayerColor(c)
|
gameWorld:switchPlayerColor(currentRoom, c)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- if key == "w" then
|
-- if key == "w" then
|
||||||
|
|
|
||||||
97
room.lua
97
room.lua
|
|
@ -132,6 +132,7 @@ function Room:draw()
|
||||||
for _, channel in ipairs(self.colorsPlayerHas) do
|
for _, channel in ipairs(self.colorsPlayerHas) do
|
||||||
if self.player[channel] then self.player[channel]:draw(channel) end
|
if self.player[channel] then self.player[channel]:draw(channel) end
|
||||||
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
|
for _, entity in pairs(self.collideables[channel]) do entity:draw(channel) end
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then gameWorld:drawOverlaps(self, channel) end
|
||||||
for _, blocker in pairs(self.playerBlockers[channel]) do blocker: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 _, switch in pairs(self.switches[channel]) do switch:draw(channel) end
|
||||||
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
|
for _, npc in ipairs(self.npcs) do npc:draw(channel) end
|
||||||
|
|
@ -181,7 +182,7 @@ function Room:createEntityMatrix(entities)
|
||||||
for _, entity in pairs(entities) do
|
for _, entity in pairs(entities) do
|
||||||
for _, occupiedCells in pairs(entity:getOccupiedCells()) do
|
for _, occupiedCells in pairs(entity:getOccupiedCells()) do
|
||||||
--we store a table reference so we can call a method on it during a chec_
|
--we store a table reference so we can call a method on it during a chec_
|
||||||
matrix[occupiedCells.y][occupiedCells.x] = entity
|
if self:isInBounds(occupiedCells) then matrix[occupiedCells.y][occupiedCells.x] = entity end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- printMatrix(matrix)
|
-- printMatrix(matrix)
|
||||||
|
|
@ -193,7 +194,9 @@ function Room:getCellInMatrix(matrix, cell)
|
||||||
return matrix[cell.y][cell.x]
|
return matrix[cell.y][cell.x]
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:movePlayer(key)
|
-- Kept for isolated room tests/editor previews. The live game goes through
|
||||||
|
-- World:movePlayer so no gameplay decision is room-local.
|
||||||
|
function Room:movePlayerLocal(key)
|
||||||
|
|
||||||
local moved = false
|
local moved = false
|
||||||
local previousPositions = {} --these are so we can perform the overlap chec_
|
local previousPositions = {} --these are so we can perform the overlap chec_
|
||||||
|
|
@ -224,23 +227,36 @@ function Room:movePlayer(key)
|
||||||
x = pos.x + dir.x, y = pos.y + dir.y
|
x = pos.x + dir.x, y = pos.y + dir.y
|
||||||
} -- this is for reference for each color as we change the targetCells table
|
} -- this is for reference for each color as we change the targetCells table
|
||||||
|
|
||||||
|
local useWorld = gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self
|
||||||
|
local context = useWorld and gameWorld:movementContext(self, color)
|
||||||
|
local worldTarget = useWorld and context:worldCell(initialTargetCell)
|
||||||
local initialTargetEntity
|
local initialTargetEntity
|
||||||
|
|
||||||
if self:isInBounds(initialTargetCell) then
|
if useWorld then
|
||||||
|
initialTargetEntity = context:entityAt(worldTarget)
|
||||||
|
elseif self:isInBounds(initialTargetCell) then
|
||||||
initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
|
initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
|
||||||
end
|
end
|
||||||
|
|
||||||
if initialTargetEntity then initialTargetEntity:bump() end
|
if initialTargetEntity then initialTargetEntity:bump() end
|
||||||
local playerBlocker
|
local playerBlocker
|
||||||
if self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then
|
if useWorld then
|
||||||
|
playerBlocker = context:playerBlockerAt(worldTarget)
|
||||||
|
elseif self:isInBounds(initialTargetCell) and self.playerBlockerMatrices and self.playerBlockerMatrices[color] then
|
||||||
playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell)
|
playerBlocker = self:getCellInMatrix(self.playerBlockerMatrices[color], initialTargetCell)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self:isInBounds(initialTargetCell) then
|
if useWorld and not context:isInBounds(worldTarget) then
|
||||||
|
move = true
|
||||||
|
elseif not useWorld and not self:isInBounds(initialTargetCell) then
|
||||||
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
-- let's us move out of bounds to Change Rooms without pulling a bloc_
|
||||||
move = true
|
move = true
|
||||||
elseif playerBlocker and playerBlocker:blocksPlayer() then
|
elseif playerBlocker and playerBlocker:blocksPlayer() then
|
||||||
move = false
|
move = false
|
||||||
|
elseif useWorld then
|
||||||
|
move, entitiesToPush = Movement.findMovableGroup({worldTarget}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
else
|
else
|
||||||
move, entitiesToPush = Movement.findMovableGroup(
|
move, entitiesToPush = Movement.findMovableGroup(
|
||||||
{initialTargetCell}, dir,
|
{initialTargetCell}, dir,
|
||||||
|
|
@ -253,7 +269,11 @@ function Room:movePlayer(key)
|
||||||
|
|
||||||
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}
|
||||||
local pull = false
|
local pull = false
|
||||||
if self:isInBounds(initialTargetCell) then
|
if useWorld and context:isInBounds(context:worldCell(initialTargetCell)) then
|
||||||
|
pull, entitiesToPull = Movement.findMovableGroup({context:worldCell(initialTargetCell)}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
elseif not useWorld and self:isInBounds(initialTargetCell) then
|
||||||
pull, entitiesToPull = Movement.findMovableGroup(
|
pull, entitiesToPull = Movement.findMovableGroup(
|
||||||
{initialTargetCell}, dir,
|
{initialTargetCell}, dir,
|
||||||
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
function(cell) return self:getCellInMatrix(self.collidableMatrices[color], cell) end,
|
||||||
|
|
@ -262,10 +282,15 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
if move then
|
if move then
|
||||||
for _, entity in pairs(entitiesToPush) do
|
if useWorld then
|
||||||
entity:move(axis, dir[axis])
|
local group = {}
|
||||||
|
for entity in pairs(entitiesToPush) do group[entity] = entity end
|
||||||
|
if pull then for entity in pairs(entitiesToPull) do group[entity] = entity end end
|
||||||
|
gameWorld:moveGroup(group, dir)
|
||||||
|
else
|
||||||
|
for _, entity in pairs(entitiesToPush) do entity:move(axis, dir[axis]) end
|
||||||
end
|
end
|
||||||
if pull == true then
|
if pull == true and not useWorld then
|
||||||
for _, entity in pairs(entitiesToPull) do
|
for _, entity in pairs(entitiesToPull) do
|
||||||
-- A connected component can wrap around the player and be found by
|
-- A connected component can wrap around the player and be found by
|
||||||
-- both searches. Every entity still moves exactly one grid cell.
|
-- both searches. Every entity still moves exactly one grid cell.
|
||||||
|
|
@ -281,12 +306,20 @@ function Room:movePlayer(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
if moved then
|
if moved then
|
||||||
self:playerJoinCheck(previousPositions, newPositions)
|
if useWorld then gameWorld:playerJoinCheck(self, previousPositions, newPositions)
|
||||||
|
else self:playerJoinCheck(previousPositions, newPositions) end
|
||||||
self:tick()
|
self:tick()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:switchPlayerColor(color)
|
function Room:movePlayer(key)
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
return gameWorld:movePlayer(self, key)
|
||||||
|
end
|
||||||
|
return self:movePlayerLocal(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Room:switchPlayerColorLocal(color)
|
||||||
-- Number keys can request a colour the player has not collected yet. Do
|
-- Number keys can request a colour the player has not collected yet. Do
|
||||||
-- nothing in that case: there is no corresponding player object to switch
|
-- nothing in that case: there is no corresponding player object to switch
|
||||||
-- to (or use as the sound source).
|
-- to (or use as the sound source).
|
||||||
|
|
@ -304,6 +337,13 @@ function Room:switchPlayerColor(color)
|
||||||
return canSwitch
|
return canSwitch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Room:switchPlayerColor(color)
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
return gameWorld:switchPlayerColor(self, color)
|
||||||
|
end
|
||||||
|
return self:switchPlayerColorLocal(color)
|
||||||
|
end
|
||||||
|
|
||||||
function Room:setActiveColor(color, bool)
|
function Room:setActiveColor(color, bool)
|
||||||
self.activeColors[color] = bool
|
self.activeColors[color] = bool
|
||||||
end
|
end
|
||||||
|
|
@ -361,7 +401,7 @@ function Room:onlyColorIsActive(color)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Room:tick()
|
function Room:tickLocal()
|
||||||
for color, entityArray in pairs(self.collideables) do
|
for color, entityArray in pairs(self.collideables) do
|
||||||
for _, entity in pairs(entityArray) do
|
for _, entity in pairs(entityArray) do
|
||||||
entity:tick()
|
entity:tick()
|
||||||
|
|
@ -371,6 +411,11 @@ function Room:tick()
|
||||||
self:createPlayerBlockerMatrix()
|
self:createPlayerBlockerMatrix()
|
||||||
self:createSwitchMatrix()
|
self:createSwitchMatrix()
|
||||||
self:resolveSwitchesAndDoors()
|
self:resolveSwitchesAndDoors()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Room:tick()
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then return gameWorld:tick(self) end
|
||||||
|
self:tickLocal()
|
||||||
self:nextRoomCheck()
|
self:nextRoomCheck()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -445,7 +490,14 @@ function Room:switchCheck()
|
||||||
for color, switchArray in pairs(self.switches) do
|
for color, switchArray in pairs(self.switches) do
|
||||||
local onSwitches = {}
|
local onSwitches = {}
|
||||||
for _, switch in pairs(switchArray) do
|
for _, switch in pairs(switchArray) do
|
||||||
local targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
|
local targetCell
|
||||||
|
if gameWorld and gameWorld.levels and gameWorld.levels[self.name] == self then
|
||||||
|
local cell = gameWorld:worldCell(self, switch:getGridPos())
|
||||||
|
local row = cell and gameWorld.collidableMatrices[color][cell.y]
|
||||||
|
targetCell = row and row[cell.x]
|
||||||
|
else
|
||||||
|
targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
|
||||||
|
end
|
||||||
local player = self.player[color]
|
local player = self.player[color]
|
||||||
-- Players are not stored in collidableMatrices, but a colour copy
|
-- Players are not stored in collidableMatrices, but a colour copy
|
||||||
-- standing on its matching switch presses it just like a block does.
|
-- standing on its matching switch presses it just like a block does.
|
||||||
|
|
@ -769,6 +821,7 @@ function Room:serialize(includePlayers)
|
||||||
end
|
end
|
||||||
|
|
||||||
function nextRoom(pos)
|
function nextRoom(pos)
|
||||||
|
if gameWorld and currentRoom then return gameWorld:nextRoom(currentRoom, pos) end
|
||||||
local exit
|
local exit
|
||||||
local newPos = {x = pos.x, y = pos.y}
|
local newPos = {x = pos.x, y = pos.y}
|
||||||
if pos.x > gridWidth then
|
if pos.x > gridWidth then
|
||||||
|
|
@ -791,13 +844,17 @@ function nextRoom(pos)
|
||||||
|
|
||||||
-- Missing map neighbours intentionally wrap back into the room just exited.
|
-- Missing map neighbours intentionally wrap back into the room just exited.
|
||||||
-- A placed neighbour gets the same carried player state at its opposite edge.
|
-- A placed neighbour gets the same carried player state at its opposite edge.
|
||||||
currentRoom = Room:new{
|
local playerState = {x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()}
|
||||||
name = nextName or currentName,
|
currentRoom = gameWorld and gameWorld.levels[nextName or currentName] or Room:new{name = nextName or currentName, runtime = true}
|
||||||
runtime = true,
|
if currentRoom then
|
||||||
player = {
|
currentRoom.activeColors = {red = false, green = false, blue = false}
|
||||||
x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
|
for _, color in ipairs(playerState.activeColors) do
|
||||||
}
|
if currentRoom.player[color] then currentRoom.player[color].x, currentRoom.player[color].y = playerState.x, playerState.y
|
||||||
}
|
else currentRoom:registerPlayer(Player:new(playerState.x, playerState.y, color), color) end
|
||||||
|
currentRoom:setActiveColor(color, true)
|
||||||
|
end
|
||||||
|
gameWorld:playerJoinCheck(currentRoom)
|
||||||
|
end
|
||||||
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end
|
||||||
if currentRoom.name then setEditorRoomName(currentRoom.name) end
|
if currentRoom.name then setEditorRoomName(currentRoom.name) end
|
||||||
|
|
||||||
|
|
|
||||||
295
world.lua
295
world.lua
|
|
@ -1,5 +1,7 @@
|
||||||
World = class("World")
|
World = class("World")
|
||||||
|
|
||||||
|
local Movement = require "movement"
|
||||||
|
|
||||||
-- The world is deliberately only a layout: rooms continue to own their puzzle
|
-- 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
|
-- state. Keeping the two separate lets the editor move a room without
|
||||||
-- rewriting its level file.
|
-- rewriting its level file.
|
||||||
|
|
@ -10,6 +12,13 @@ function World:initialize()
|
||||||
self.previewCache = {}
|
self.previewCache = {}
|
||||||
self.lastRoom = nil
|
self.lastRoom = nil
|
||||||
self:load()
|
self:load()
|
||||||
|
self.levels = {}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:loadLevels()
|
||||||
|
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name} end
|
||||||
|
self:rebuildCollision()
|
||||||
end
|
end
|
||||||
|
|
||||||
function World:listRoomNames()
|
function World:listRoomNames()
|
||||||
|
|
@ -128,6 +137,292 @@ function World:getNeighbor(name, exit)
|
||||||
return self:roomAt(room.x + direction.x, room.y + direction.y)
|
return self:roomAt(room.x + direction.x, room.y + direction.y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- All placed levels share these sparse matrices. Rendering still uses the
|
||||||
|
-- selected Room, whose ordinary local matrices remain unchanged.
|
||||||
|
function World:rebuildCollision()
|
||||||
|
self.collidableMatrices = {red = {}, green = {}, blue = {}}
|
||||||
|
self.playerBlockerMatrices = {red = {}, green = {}, blue = {}}
|
||||||
|
self.entityRooms = {}
|
||||||
|
local function put(matrix, cell, entity)
|
||||||
|
matrix[cell.y] = matrix[cell.y] or {}
|
||||||
|
matrix[cell.y][cell.x] = entity
|
||||||
|
end
|
||||||
|
for name, room in pairs(self.levels) do
|
||||||
|
local location = self:roomLocation(name)
|
||||||
|
if location then
|
||||||
|
for color, entities in pairs(room.collideables) do
|
||||||
|
for _, entity in pairs(entities) do
|
||||||
|
self.entityRooms[entity] = room
|
||||||
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||||
|
put(self.collidableMatrices[color], {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}, entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- NPCs are local room state, but globally collidable: a movement lookup
|
||||||
|
-- must find them so its normal bump() path can open dialogue.
|
||||||
|
for _, npc in ipairs(room.npcs) do
|
||||||
|
self.entityRooms[npc] = room
|
||||||
|
for _, cell in ipairs(npc:getOccupiedCells()) do
|
||||||
|
local worldCell = {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}
|
||||||
|
for color in pairs(self.collidableMatrices) do put(self.collidableMatrices[color], worldCell, npc) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for color, entities in pairs(room.playerBlockers) do
|
||||||
|
for _, entity in pairs(entities) do
|
||||||
|
for _, cell in ipairs(entity:getOccupiedCells()) do
|
||||||
|
put(self.playerBlockerMatrices[color], {
|
||||||
|
x = (location.x - 1) * gridWidth + cell.x,
|
||||||
|
y = (location.y - 1) * gridHeight + cell.y,
|
||||||
|
}, entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:worldCell(room, cell)
|
||||||
|
local location = self:roomLocation(room.name)
|
||||||
|
return location and {x = (location.x - 1) * gridWidth + cell.x, y = (location.y - 1) * gridHeight + cell.y}
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:roomCell(cell)
|
||||||
|
local roomX = math.floor((cell.x - 1) / gridWidth) + 1
|
||||||
|
local roomY = math.floor((cell.y - 1) / gridHeight) + 1
|
||||||
|
local name = self:roomAt(roomX, roomY)
|
||||||
|
local room = name and self.levels[name]
|
||||||
|
return room, {x = cell.x - (roomX - 1) * gridWidth, y = cell.y - (roomY - 1) * gridHeight}
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:movementContext(room, color)
|
||||||
|
local proxies = {}
|
||||||
|
local world = self
|
||||||
|
local context = {}
|
||||||
|
function context:worldCell(cell) return world:worldCell(room, cell) end
|
||||||
|
function context:isInBounds(cell) return world:roomCell(cell) ~= nil end
|
||||||
|
function context:entityAt(cell)
|
||||||
|
local row = world.collidableMatrices[color][cell.y]
|
||||||
|
local entity = row and row[cell.x]
|
||||||
|
if not entity then return nil end
|
||||||
|
if proxies[entity] then return proxies[entity] end
|
||||||
|
local owner = world.entityRooms[entity]
|
||||||
|
local position = world:worldCell(owner, entity:getGridPos())
|
||||||
|
local proxy = {entity = entity, room = owner, color = color, x = position.x, y = position.y}
|
||||||
|
function proxy:canMove() return self.entity:canMove() end
|
||||||
|
function proxy:canPassOver() return self.entity:canPassOver() end
|
||||||
|
function proxy:isSticky() return self.entity:isSticky() end
|
||||||
|
function proxy:isSkewer() return self.entity:isSkewer() end
|
||||||
|
function proxy:isSpiky() return self.entity:isSpiky() end
|
||||||
|
function proxy:bump() return self.entity:bump() end
|
||||||
|
function proxy:getOccupiedCells(offset)
|
||||||
|
offset = offset or {x = 0, y = 0}
|
||||||
|
local cells = {}
|
||||||
|
for _, shapeCell in ipairs(self.entity:getCellShape()) do
|
||||||
|
table.insert(cells, {x = self.x + shapeCell.x - 1 + offset.x, y = self.y + shapeCell.y - 1 + offset.y})
|
||||||
|
end
|
||||||
|
return cells
|
||||||
|
end
|
||||||
|
proxies[entity] = proxy
|
||||||
|
return proxy
|
||||||
|
end
|
||||||
|
function context:playerBlockerAt(cell)
|
||||||
|
local row = world.playerBlockerMatrices[color][cell.y]
|
||||||
|
return row and row[cell.x]
|
||||||
|
end
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Player copies live in room files, but joining is a world-coordinate rule.
|
||||||
|
-- This also lets a copy rejoin immediately when the player crosses into its
|
||||||
|
-- room, rather than waiting for one more local move.
|
||||||
|
function World:playerJoinCheck(room, previousPositions, newPositions)
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
local deactivate, activate = {}, {}
|
||||||
|
if previousPositions and newPositions and #activeColors > 1 then
|
||||||
|
for _, color in ipairs(activeColors) do
|
||||||
|
if Entity.positionsAreEqual(previousPositions[color], newPositions[color]) then
|
||||||
|
table.insert(deactivate, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for color, player in pairs(room.player) do
|
||||||
|
if not room:colorIsActive(color) then
|
||||||
|
local playerCell = self:worldCell(room, player:getGridPos())
|
||||||
|
local overlaps = playerCell and #activeColors > 0
|
||||||
|
for _, activeColor in ipairs(activeColors) do
|
||||||
|
local activeCell = self:worldCell(room, room.player[activeColor]:getGridPos())
|
||||||
|
if not activeCell or activeCell.x ~= playerCell.x or activeCell.y ~= playerCell.y then overlaps = false; break end
|
||||||
|
end
|
||||||
|
if overlaps then table.insert(activate, color) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, color in ipairs(activate) do room:setActiveColor(color, true) end
|
||||||
|
for _, color in ipairs(deactivate) do room:setActiveColor(color, false) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:movePlayer(room, key)
|
||||||
|
local directions = {
|
||||||
|
up = {x = 0, y = -1, axis = "y"}, down = {x = 0, y = 1, axis = "y"},
|
||||||
|
left = {x = -1, y = 0, axis = "x"}, right = {x = 1, y = 0, axis = "x"},
|
||||||
|
}
|
||||||
|
local dir = directions[key]
|
||||||
|
if not dir then return end
|
||||||
|
local moved, previous, current = false, {}, {}
|
||||||
|
for _, color in ipairs(room:getActiveColors()) do
|
||||||
|
local player = room.player[color]
|
||||||
|
local pos = player:getGridPos()
|
||||||
|
previous[color] = pos
|
||||||
|
local context = self:movementContext(room, color)
|
||||||
|
local target = context:worldCell({x = pos.x + dir.x, y = pos.y + dir.y})
|
||||||
|
local targetEntity = context:entityAt(target)
|
||||||
|
if targetEntity then targetEntity:bump() end
|
||||||
|
local blocker = context:playerBlockerAt(target)
|
||||||
|
local canMove, pushing = true, {}
|
||||||
|
if context:isInBounds(target) and not (blocker and blocker:blocksPlayer()) then
|
||||||
|
canMove, pushing = Movement.findMovableGroup({target}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
elseif blocker then
|
||||||
|
canMove = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local pulling, pullingGroup = false, {}
|
||||||
|
local behind = context:worldCell({x = pos.x - dir.x, y = pos.y - dir.y})
|
||||||
|
if context:isInBounds(behind) then
|
||||||
|
pulling, pullingGroup = Movement.findMovableGroup({behind}, dir,
|
||||||
|
function(cell) return context:entityAt(cell) end,
|
||||||
|
function(cell) return context:isInBounds(cell) end)
|
||||||
|
end
|
||||||
|
if canMove then
|
||||||
|
local group = {}
|
||||||
|
for entity in pairs(pushing) do group[entity] = entity end
|
||||||
|
if pulling then for entity in pairs(pullingGroup) do group[entity] = entity end end
|
||||||
|
self:moveGroup(group, dir)
|
||||||
|
player:move(dir.axis, dir[dir.axis])
|
||||||
|
moved = true
|
||||||
|
end
|
||||||
|
current[color] = player:getGridPos()
|
||||||
|
end
|
||||||
|
if moved then
|
||||||
|
self:playerJoinCheck(room, previous, current)
|
||||||
|
self:tick(room)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:switchPlayerColor(room, color)
|
||||||
|
local player = room.player[color]
|
||||||
|
if not player then return false end
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
local canSwitch = #activeColors ~= 1 or activeColors[1] ~= color
|
||||||
|
if canSwitch then
|
||||||
|
for _, possibleColor in ipairs(room:getPossibleColors()) do
|
||||||
|
room:setActiveColor(possibleColor, possibleColor == color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
player:playSwitchSound(canSwitch)
|
||||||
|
return canSwitch
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:tick(room)
|
||||||
|
room:tickLocal()
|
||||||
|
self:rebuildCollision()
|
||||||
|
self:nextRoomCheck(room)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:nextRoomCheck(room)
|
||||||
|
for _, color in ipairs(room:getActiveColors()) do
|
||||||
|
local player = room.player[color]
|
||||||
|
if player and not room:isInBounds(player:getGridPos()) then
|
||||||
|
self:nextRoom(room, player:getGridPos())
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:nextRoom(room, pos)
|
||||||
|
local exit, entry = nil, {x = pos.x, y = pos.y}
|
||||||
|
if pos.x > gridWidth then exit, entry.x = "right", 1
|
||||||
|
elseif pos.x < 1 then exit, entry.x = "left", gridWidth
|
||||||
|
elseif pos.y > gridHeight then exit, entry.y = "bottom", 1
|
||||||
|
elseif pos.y < 1 then exit, entry.y = "top", gridHeight end
|
||||||
|
local nextName = self:getNeighbor(room.name, exit) or room.name
|
||||||
|
local activeColors = room:getActiveColors()
|
||||||
|
room:save()
|
||||||
|
room.activeColors = {red = false, green = false, blue = false}
|
||||||
|
currentRoom = self.levels[nextName]
|
||||||
|
for _, color in ipairs(activeColors) do
|
||||||
|
if currentRoom.player[color] then
|
||||||
|
currentRoom.player[color].x, currentRoom.player[color].y = entry.x, entry.y
|
||||||
|
else
|
||||||
|
currentRoom:registerPlayer(Player:new(entry.x, entry.y, color), color)
|
||||||
|
end
|
||||||
|
currentRoom:setActiveColor(color, true)
|
||||||
|
end
|
||||||
|
self:playerJoinCheck(currentRoom)
|
||||||
|
self:setLastRoom(currentRoom.name)
|
||||||
|
setEditorRoomName(currentRoom.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:moveGroup(group, direction)
|
||||||
|
local changed = {}
|
||||||
|
for proxy in pairs(group) do
|
||||||
|
changed[proxy.room] = true
|
||||||
|
for _, cell in ipairs(proxy:getOccupiedCells()) do
|
||||||
|
local room = self:roomCell(cell)
|
||||||
|
if room then changed[room] = true end
|
||||||
|
end
|
||||||
|
for _, cell in ipairs(proxy:getOccupiedCells(direction)) do
|
||||||
|
local room = self:roomCell(cell)
|
||||||
|
if room then changed[room] = true end
|
||||||
|
end
|
||||||
|
local destination, localCell = self:roomCell({x = proxy.x + direction.x, y = proxy.y + direction.y})
|
||||||
|
if destination then
|
||||||
|
if destination ~= proxy.room then
|
||||||
|
for i = #proxy.room.collideables[proxy.color], 1, -1 do
|
||||||
|
if proxy.room.collideables[proxy.color][i] == proxy.entity then table.remove(proxy.room.collideables[proxy.color], i); break end
|
||||||
|
end
|
||||||
|
table.insert(destination.collideables[proxy.color], proxy.entity)
|
||||||
|
self.entityRooms[proxy.entity] = destination
|
||||||
|
changed[proxy.room], changed[destination] = true, true
|
||||||
|
end
|
||||||
|
proxy.entity.x, proxy.entity.y = localCell.x, localCell.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:rebuildCollision()
|
||||||
|
for room in pairs(changed) do room:resolveSwitchesAndDoors() end
|
||||||
|
self:rebuildCollision()
|
||||||
|
for room in pairs(changed) do if room ~= currentRoom then room:save() end end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:drawOverlaps(room, color)
|
||||||
|
local location = self:roomLocation(room.name)
|
||||||
|
if not location then return end
|
||||||
|
local drawn = {}
|
||||||
|
for y = 1, gridHeight do
|
||||||
|
local row = self.collidableMatrices[color][(location.y - 1) * gridHeight + y]
|
||||||
|
if row then
|
||||||
|
for x = 1, gridWidth do
|
||||||
|
local entity = row[(location.x - 1) * gridWidth + x]
|
||||||
|
local owner = entity and self.entityRooms[entity]
|
||||||
|
if entity and owner ~= room and not drawn[entity] then
|
||||||
|
drawn[entity] = true
|
||||||
|
local ownerLocation = self:roomLocation(owner.name)
|
||||||
|
love.graphics.push()
|
||||||
|
love.graphics.translate((ownerLocation.x - location.x) * width, (ownerLocation.y - location.y) * height)
|
||||||
|
entity:draw(color)
|
||||||
|
love.graphics.pop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function World:getCellFromMousePos(mouseX, mouseY)
|
function World:getCellFromMousePos(mouseX, mouseY)
|
||||||
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
|
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue