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
295
world.lua
295
world.lua
|
|
@ -1,5 +1,7 @@
|
|||
World = class("World")
|
||||
|
||||
local Movement = require "movement"
|
||||
|
||||
-- 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
|
||||
-- rewriting its level file.
|
||||
|
|
@ -10,6 +12,13 @@ function World:initialize()
|
|||
self.previewCache = {}
|
||||
self.lastRoom = nil
|
||||
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
|
||||
|
||||
function World:listRoomNames()
|
||||
|
|
@ -128,6 +137,292 @@ function World:getNeighbor(name, exit)
|
|||
return self:roomAt(room.x + direction.x, room.y + direction.y)
|
||||
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)
|
||||
return math.floor(mouseX / (width / self.width)) + 1, math.floor(mouseY / (height / self.height)) + 1
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue