176 lines
4.4 KiB
Lua
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
|