580 lines
20 KiB
Lua
580 lines
20 KiB
Lua
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.
|
|
function World:initialize()
|
|
self.width, self.height = 18, 18
|
|
self.rooms = {}
|
|
self.roomByName = {}
|
|
self.previewCache = {}
|
|
self.lastRoom = nil
|
|
self:load()
|
|
self:loadRuntimeState()
|
|
self.levels = {}
|
|
|
|
end
|
|
|
|
function World:loadLevels()
|
|
-- Gameplay saves go to LÖVE's runtime directory. Seed every world slice
|
|
-- through the same runtime-first loader, not just the initially visible one.
|
|
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name, runtime = true} end
|
|
self:rebuildCollision()
|
|
end
|
|
|
|
-- The editor can replace a level with a freshly loaded authored Room. Keep the
|
|
-- global simulation pointing at that exact instance, not a stale preload.
|
|
function World:setLevel(room)
|
|
if not room or not room.name or not self:roomLocation(room.name) then return false end
|
|
self.levels[room.name] = room
|
|
self:rebuildCollision()
|
|
return true
|
|
end
|
|
|
|
-- Runtime state is one logical world snapshot: each level slice is written to
|
|
-- LÖVE's save directory, and this companion file says which slice was active.
|
|
-- The authored world layout remains source-only for the editor.
|
|
function World:loadRuntimeState()
|
|
local raw = love.filesystem.read("rooms/world.sav")
|
|
if not raw then return end
|
|
local ok, state = pcall(TSerial.unpack, raw)
|
|
if ok and type(state) == "table" and state.lastRoom and self:roomLocation(state.lastRoom) then
|
|
self.lastRoom = state.lastRoom
|
|
end
|
|
end
|
|
|
|
function World:saveRuntime(dirtyRooms)
|
|
for room in pairs(dirtyRooms or {}) do room:save() end
|
|
return love.filesystem.write("rooms/world.sav", TSerial.pack({lastRoom = currentRoom and currentRoom.name}, nil, true))
|
|
end
|
|
|
|
function World:markDirty(room)
|
|
self.dirtyRooms = self.dirtyRooms or {}
|
|
self.dirtyRooms[room] = true
|
|
end
|
|
|
|
function World:flushRuntime()
|
|
self:saveRuntime(self.dirtyRooms)
|
|
self.dirtyRooms = {}
|
|
end
|
|
|
|
function World:listRoomNames()
|
|
local names = {}
|
|
for _, file in ipairs(love.filesystem.getDirectoryItems("rooms")) do
|
|
local name = file:match("^(.+)%.sav$")
|
|
if name and name ~= "world" then table.insert(names, name) end
|
|
end
|
|
table.sort(names)
|
|
return names
|
|
end
|
|
|
|
function World:load()
|
|
local raw = readFromSource("rooms/world.sav")
|
|
if not raw then return end
|
|
local ok, save = pcall(TSerial.unpack, raw)
|
|
if not ok or type(save) ~= "table" then return end
|
|
self.lastRoom = save.lastRoom
|
|
for _, room in ipairs(save.rooms or {}) do
|
|
if room.name and self:isInBounds(room.x, room.y) then
|
|
self:_putRoom(room.name, room.x, room.y)
|
|
end
|
|
end
|
|
end
|
|
|
|
function World:save()
|
|
local rooms = {}
|
|
for _, room in ipairs(self.rooms) do
|
|
table.insert(rooms, { name = room.name, x = room.x, y = room.y })
|
|
end
|
|
return writeToSource("rooms/world.sav", TSerial.pack({ width = self.width, height = self.height, lastRoom = self.lastRoom, rooms = rooms }, nil, true))
|
|
end
|
|
|
|
function World:isInBounds(x, y)
|
|
return x and y and x >= 1 and x <= self.width and y >= 1 and y <= self.height
|
|
end
|
|
|
|
function World:roomAt(x, y)
|
|
for _, room in ipairs(self.rooms) do
|
|
if room.x == x and room.y == y then return room.name end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function World:roomLocation(name)
|
|
return self.roomByName[name]
|
|
end
|
|
|
|
function World:_putRoom(name, x, y)
|
|
local existing = self.roomByName[name]
|
|
if existing then
|
|
for i = #self.rooms, 1, -1 do
|
|
if self.rooms[i] == existing then table.remove(self.rooms, i) end
|
|
end
|
|
end
|
|
for i = #self.rooms, 1, -1 do
|
|
if self.rooms[i].x == x and self.rooms[i].y == y then
|
|
self.roomByName[self.rooms[i].name] = nil
|
|
table.remove(self.rooms, i)
|
|
end
|
|
end
|
|
local room = { name = name, x = x, y = y }
|
|
table.insert(self.rooms, room)
|
|
self.roomByName[name] = room
|
|
return room
|
|
end
|
|
|
|
function World:placeRoom(name, x, y)
|
|
if not name or not self:isInBounds(x, y) then return false end
|
|
self:_putRoom(name, x, y)
|
|
return self:save()
|
|
end
|
|
|
|
function World:removeRoom(x, y)
|
|
for i = #self.rooms, 1, -1 do
|
|
local room = self.rooms[i]
|
|
if room.x == x and room.y == y then
|
|
self.roomByName[room.name] = nil
|
|
table.remove(self.rooms, i)
|
|
if self.lastRoom == room.name then self.lastRoom = nil end
|
|
return self:save(), room.name
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function World:renameRoom(oldName, newName)
|
|
local room = self:roomLocation(oldName)
|
|
if not room or oldName == newName then return false end
|
|
self.roomByName[oldName] = nil
|
|
room.name = newName
|
|
self.roomByName[newName] = room
|
|
if self.lastRoom == oldName then self.lastRoom = newName end
|
|
self:invalidatePreview(oldName)
|
|
self:invalidatePreview(newName)
|
|
return self:save()
|
|
end
|
|
|
|
function World:setLastRoom(name)
|
|
if name and self.lastRoom ~= name then
|
|
self.lastRoom = name
|
|
end
|
|
return true
|
|
end
|
|
|
|
function World:getNeighbor(name, exit)
|
|
local room = self:roomLocation(name)
|
|
if not room then return nil end
|
|
local directions = {
|
|
left = { x = -1, y = 0 }, right = { x = 1, y = 0 },
|
|
top = { x = 0, y = -1 }, bottom = { x = 0, y = 1 },
|
|
}
|
|
local direction = directions[exit]
|
|
if not direction then return nil end
|
|
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:markDirty(room)
|
|
self:playerJoinCheck(room, previous, current)
|
|
self:tick(room)
|
|
self:flushRuntime()
|
|
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)
|
|
if canSwitch then self:markDirty(room); self:flushRuntime() end
|
|
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()
|
|
self:markDirty(room)
|
|
local destination = self.levels[nextName]
|
|
for _, color in ipairs(activeColors) do
|
|
local player = room.player[color]
|
|
-- Active copies travel with the player. Inactive copies are deliberately
|
|
-- left in their room so World:playerJoinCheck can merge them later.
|
|
room.player[color] = nil
|
|
for i = #room.colorsPlayerHas, 1, -1 do
|
|
if room.colorsPlayerHas[i] == color then table.remove(room.colorsPlayerHas, i) end
|
|
end
|
|
if destination.player[color] then
|
|
for i = #destination.colorsPlayerHas, 1, -1 do
|
|
if destination.colorsPlayerHas[i] == color then table.remove(destination.colorsPlayerHas, i) end
|
|
end
|
|
end
|
|
destination.player[color] = player
|
|
table.insert(destination.colorsPlayerHas, color)
|
|
end
|
|
room.activeColors = {red = false, green = false, blue = false}
|
|
currentRoom = destination
|
|
self:markDirty(currentRoom)
|
|
for _, color in ipairs(activeColors) do
|
|
currentRoom.player[color].x, currentRoom.player[color].y = entry.x, entry.y
|
|
currentRoom:setActiveColor(color, true)
|
|
end
|
|
self:playerJoinCheck(currentRoom)
|
|
self.lastRoom = 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 self:markDirty(room) 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
|
|
|
|
function World:getRoomData(name)
|
|
-- The current room is live: its preview reflects unsaved edits immediately.
|
|
if currentRoom and currentRoom.name == name then return currentRoom:serialize() end
|
|
if not self.previewCache[name] then
|
|
local raw = readFromSource("rooms/" .. name .. ".sav")
|
|
if not raw then return nil end
|
|
local ok, room = pcall(TSerial.unpack, raw)
|
|
if not ok then return nil end
|
|
self.previewCache[name] = room
|
|
end
|
|
return self.previewCache[name]
|
|
end
|
|
|
|
function World:invalidatePreview(name)
|
|
self.previewCache[name] = nil
|
|
end
|
|
|
|
function World:drawRoomPreview(name, x, y, size)
|
|
local room = self:getRoomData(name)
|
|
if not room then return end
|
|
local scale = size / (gridWidth * 16)
|
|
love.graphics.setScissor(x, y, size, size)
|
|
for _, object in ipairs(room.objects or {}) do
|
|
if object.class == "npc" then
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.print("?", x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
|
else
|
|
local props = globalAssetProperties[object.class]
|
|
if props and props.sprites then
|
|
local colors
|
|
if object.class == "door" then
|
|
colors = object.color and { object.color } or { "red", "green", "blue" }
|
|
else
|
|
colors = { object.color }
|
|
end
|
|
for _, color in ipairs(colors) do
|
|
local path = props.sprites[color]
|
|
if path then
|
|
love.graphics.setColor(gColor[color]:set())
|
|
local sprite = loadSprite(path)
|
|
if props.goopy and drawGoopySprite then
|
|
drawGoopySprite(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
|
elseif props.skewer and drawSkewerSprite then
|
|
drawSkewerSprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
|
elseif props.spiky and drawSpikySprite then
|
|
drawSpikySprite(sprite, props.cells and props.cells[color], x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, scale)
|
|
else
|
|
love.graphics.draw(sprite, x + (object.x - 1) * 16 * scale, y + (object.y - 1) * 16 * scale, 0, scale, scale)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
love.graphics.setScissor()
|
|
end
|
|
|
|
function World:drawHoveredRoomName()
|
|
local mouseX, mouseY = love.mouse.getPosition()
|
|
local cellX, cellY = self:getCellFromMousePos(mouseX, mouseY)
|
|
local name = self:isInBounds(cellX, cellY) and self:roomAt(cellX, cellY)
|
|
if not name then return end
|
|
|
|
local font = love.graphics.getFont()
|
|
local padding = 6
|
|
local tooltipW = math.min(font:getWidth(name) + padding * 2, width - 8)
|
|
local tooltipH = font:getHeight() + padding * 2
|
|
local tooltipX = math.min(mouseX + 14, width - tooltipW - 4)
|
|
local tooltipY = math.min(mouseY + 14, height - tooltipH - 4)
|
|
tooltipX = math.max(4, tooltipX)
|
|
tooltipY = math.max(4, tooltipY)
|
|
|
|
-- Keep the label anchored beside the pointer, while keeping it entirely in
|
|
-- the playable map rather than allowing it to spill into the editor panel.
|
|
love.graphics.setColor(0, 0, 0, 0.9)
|
|
love.graphics.rectangle("fill", tooltipX, tooltipY, tooltipW, tooltipH)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.rectangle("line", tooltipX, tooltipY, tooltipW, tooltipH)
|
|
love.graphics.printf(name, tooltipX + padding, tooltipY + padding, tooltipW - padding * 2, "left")
|
|
end
|
|
|
|
function World:draw()
|
|
local cell = width / self.width
|
|
love.graphics.setColor(12 / 255, 12 / 255, 20 / 255, 1)
|
|
love.graphics.rectangle("fill", 0, 0, width, height)
|
|
for y = 1, self.height do
|
|
for x = 1, self.width do
|
|
local screenX, screenY = (x - 1) * cell, (y - 1) * cell
|
|
local name = self:roomAt(x, y)
|
|
if name then self:drawRoomPreview(name, screenX, screenY, cell) end
|
|
love.graphics.setColor(name and 1 or 70 / 255, name and 1 or 70 / 255, name and 1 or 90 / 255, 1)
|
|
love.graphics.rectangle("line", screenX, screenY, cell, cell)
|
|
end
|
|
end
|
|
self:drawHoveredRoomName()
|
|
end
|