Files
warren/src/map.lua
T

176 lines
4.4 KiB
Lua

-- Level grid: walls, floors, stairs, doors, traps and other terrain. Tile
-- semantics (walkable, blocks sight, is a door) live in the Tiles registry;
-- the default generator places random non-overlapping rooms and links their
-- centres with L-shaped corridors.
local Tiles = require("tiles")
local Map = {}
Map.__index = Map
Map.WALL = Tiles.WALL
Map.FLOOR = Tiles.FLOOR
Map.STAIRS = Tiles.STAIRS_DOWN
Map.STAIRS_DOWN = Tiles.STAIRS_DOWN
Map.STAIRS_UP = Tiles.STAIRS_UP
Map.DOOR_CLOSED = Tiles.DOOR_CLOSED
Map.DOOR_OPEN = Tiles.DOOR_OPEN
Map.DOOR_LOCKED = Tiles.DOOR_LOCKED
Map.TRAP = Tiles.TRAP
Map.WATER = Tiles.WATER
local function newGrid(width, height, fill)
local tiles = {}
for i = 1, width * height do
tiles[i] = fill
end
return tiles
end
function Map.new(width, height)
return setmetatable({
width = width,
height = height,
tiles = newGrid(width, height, Map.WALL),
rooms = {},
stairsX = 0, stairsY = 0,
startX = 0, startY = 0,
}, Map)
end
function Map:index(x, y)
return (y - 1) * self.width + x
end
function Map:inBounds(x, y)
return x >= 1 and y >= 1 and x <= self.width and y <= self.height
end
function Map:get(x, y)
if not self:inBounds(x, y) then return Map.WALL end
return self.tiles[self:index(x, y)]
end
function Map:set(x, y, tile)
self.tiles[self:index(x, y)] = tile
end
function Map:isWalkable(x, y)
return Tiles.isWalkable(self:get(x, y))
end
function Map:blocksSight(x, y)
return Tiles.blocksSight(self:get(x, y))
end
-- Passability for pathing: closed doors count as passable so routes lead through
-- them; a locked door is only passable when the traveller holds a key.
function Map:isPassable(x, y, hasKey)
local tile = self:get(x, y)
if Tiles.isWalkable(tile) then return true end
if tile == Map.DOOR_CLOSED then return true end
if tile == Map.DOOR_LOCKED then return hasKey == true end
return false
end
local function carveRoom(map, room)
for y = room.y, room.y + room.h - 1 do
for x = room.x, room.x + room.w - 1 do
map:set(x, y, Map.FLOOR)
end
end
end
local function carveHTunnel(map, x1, x2, y)
for x = math.min(x1, x2), math.max(x1, x2) do
map:set(x, y, Map.FLOOR)
end
end
local function carveVTunnel(map, y1, y2, x)
for y = math.min(y1, y2), math.max(y1, y2) do
map:set(x, y, Map.FLOOR)
end
end
local function roomsOverlap(a, b)
return a.x <= b.x + b.w and a.x + a.w >= b.x
and a.y <= b.y + b.h and a.y + a.h >= b.y
end
local function roomCenter(room)
return room.x + room.w // 2, room.y + room.h // 2
end
function Map.generate(width, height, rng, options)
options = options or {}
local maxRooms = options.maxRooms or 12
local minSize = options.minRoomSize or 4
local maxSize = options.maxRoomSize or 8
local map = Map.new(width, height)
for _ = 1, maxRooms do
local w = rng:range(minSize, maxSize)
local h = rng:range(minSize, maxSize)
local room = {
x = rng:range(2, width - w - 1),
y = rng:range(2, height - h - 1),
w = w, h = h,
}
local fits = true
for _, other in ipairs(map.rooms) do
if roomsOverlap(room, other) then
fits = false
break
end
end
if fits then
carveRoom(map, room)
if #map.rooms > 0 then
local prevX, prevY = roomCenter(map.rooms[#map.rooms])
local newX, newY = roomCenter(room)
if rng:chance(1, 2) then
carveHTunnel(map, prevX, newX, prevY)
carveVTunnel(map, prevY, newY, newX)
else
carveVTunnel(map, prevY, newY, prevX)
carveHTunnel(map, prevX, newX, newY)
end
end
map.rooms[#map.rooms + 1] = room
end
end
map.startX, map.startY = roomCenter(map.rooms[1])
map.stairsX, map.stairsY = roomCenter(map.rooms[#map.rooms])
map:set(map.stairsX, map.stairsY, Map.STAIRS)
return map
end
function Map:toData()
return {
width = self.width,
height = self.height,
tiles = table.concat(self.tiles),
rooms = self.rooms,
stairsX = self.stairsX, stairsY = self.stairsY,
startX = self.startX, startY = self.startY,
}
end
function Map.fromData(snapshot)
local map = setmetatable({}, Map)
map.width = snapshot.width
map.height = snapshot.height
map.tiles = {}
for i = 1, #snapshot.tiles do
map.tiles[i] = snapshot.tiles:sub(i, i)
end
map.rooms = snapshot.rooms
map.stairsX, map.stairsY = snapshot.stairsX, snapshot.stairsY
map.startX, map.startY = snapshot.startX, snapshot.startY
return map
end
return Map