diff --git a/src/gen/connect.lua b/src/gen/connect.lua new file mode 100644 index 0000000..6720b9f --- /dev/null +++ b/src/gen/connect.lua @@ -0,0 +1,79 @@ +-- Guarantees every walkable cell is reachable. Labels connected components with +-- a deterministic flood fill, then tunnels each secondary component into the +-- largest one along the shortest Manhattan link. Returns the cells of the final +-- reachable region in index order. + +local Carve = require("gen.carve") + +local Connect = {} + +local NEIGHBOURS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } } + +local function label(map) + local component = {} + local components = {} + for y = 1, map.height do + for x = 1, map.width do + local idx = map:index(x, y) + if map:isWalkable(x, y) and not component[idx] then + local cells = {} + local stack = { { x, y } } + component[idx] = #components + 1 + while #stack > 0 do + local cell = stack[#stack] + stack[#stack] = nil + cells[#cells + 1] = { x = cell[1], y = cell[2] } + for _, step in ipairs(NEIGHBOURS) do + local nx, ny = cell[1] + step[1], cell[2] + step[2] + if map:inBounds(nx, ny) and map:isWalkable(nx, ny) then + local nIdx = map:index(nx, ny) + if not component[nIdx] then + component[nIdx] = #components + 1 + stack[#stack + 1] = { nx, ny } + end + end + end + end + table.sort(cells, function(a, b) + return map:index(a.x, a.y) < map:index(b.x, b.y) + end) + components[#components + 1] = cells + end + end + end + return components +end + +local function nearestPair(from, to, map) + local best, bx, by, tx, ty = math.huge + for _, a in ipairs(from) do + for _, b in ipairs(to) do + local dist = math.abs(a.x - b.x) + math.abs(a.y - b.y) + if dist < best then + best, bx, by, tx, ty = dist, a.x, a.y, b.x, b.y + end + end + end + return bx, by, tx, ty +end + +function Connect.ensure(map, rng) + local components = label(map) + if #components == 0 then return {} end + table.sort(components, function(a, b) + if #a ~= #b then return #a > #b end + return map:index(a[1].x, a[1].y) < map:index(b[1].x, b[1].y) + end) + local main = components[1] + for i = 2, #components do + local ax, ay, bx, by = nearestPair(components[i], main, map) + Carve.corridor(map, ax, ay, bx, by, rng) + end + local reachable = label(map)[1] or main + table.sort(reachable, function(a, b) + return map:index(a.x, a.y) < map:index(b.x, b.y) + end) + return reachable +end + +return Connect diff --git a/src/gen/prefab.lua b/src/gen/prefab.lua new file mode 100644 index 0000000..4130d53 --- /dev/null +++ b/src/gen/prefab.lua @@ -0,0 +1,87 @@ +-- Hand-authored vaults stamped onto a generated level. Each vault is an ASCII +-- template; markers flag where the dungeon layer should later drop treasure or +-- guardians. Stamping is deterministic given a fixed anchor. + +local Tiles = require("tiles") + +local Prefab = {} + +Prefab.vaults = { + { + name = "treasureCell", + rows = { + "#########", + "#.......#", + "#.#####.#", + "#.#*M*#.#", + "#.#*L*#+#", + "#.#####.#", + "#.......#", + "#########", + }, + }, + { + name = "guardPost", + rows = { + "#######", + "#M...M#", + "#..*..#", + "#M...M#", + "#######", + }, + }, + { + name = "flooded", + rows = { + "#########", + "#~~...~~#", + "#~..*..~#", + "#..MLM..#", + "#~..*..~#", + "#~~...~~#", + "#########", + }, + }, +} + +local glyphFor = { + ["#"] = Tiles.WALL, + ["."] = Tiles.FLOOR, + ["+"] = Tiles.DOOR_CLOSED, + ["~"] = Tiles.WATER, + ["*"] = Tiles.FLOOR, + ["M"] = Tiles.FLOOR, + ["L"] = Tiles.FLOOR, +} + +function Prefab.size(vault) + return #vault.rows[1], #vault.rows +end + +-- Returns true when a vault fits entirely inside the map interior with a wall +-- gap to the border, false otherwise. No cell content is inspected here; the +-- caller decides overlap policy. +function Prefab.fits(map, vault, ox, oy) + local w, h = Prefab.size(vault) + return ox >= 2 and oy >= 2 and ox + w - 1 <= map.width - 1 and oy + h - 1 <= map.height - 1 +end + +-- Stamps a vault at (ox, oy) and returns lists of treasure and monster anchors +-- in map coordinates so the dungeon layer can populate them. +function Prefab.stamp(map, vault, ox, oy) + local treasure, monsters, loot = {}, {}, {} + for row = 1, #vault.rows do + local line = vault.rows[row] + for col = 1, #line do + local mark = line:sub(col, col) + local x, y = ox + col - 1, oy + row - 1 + map:set(x, y, glyphFor[mark] or Tiles.FLOOR) + if mark == "*" then treasure[#treasure + 1] = { x = x, y = y } end + if mark == "M" then monsters[#monsters + 1] = { x = x, y = y } end + if mark == "L" then loot[#loot + 1] = { x = x, y = y } end + end + end + return { name = vault.name, treasure = treasure, monsters = monsters, loot = loot } +end + +return Prefab