diff --git a/src/map.lua b/src/map.lua new file mode 100644 index 0000000..1d3e9ac --- /dev/null +++ b/src/map.lua @@ -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