feat(map): add level grid with rooms and corridors
This commit is contained in:
1 parent
23ea7ff67f
commit
3f7a0cc34f
1 file changed
+175
+175
@@ -0,0 +1,175 @@
|
||||
-- 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
|
||||
Reference in new issue
Block a user