diff --git a/game/engines/default/engine/Zone.lua b/game/engines/default/engine/Zone.lua index c935e912b070a18d0beb36ae95a0d0747d4d680f..cd7e91246889f1775c80a70fe9167650953e00a7 100644 --- a/game/engines/default/engine/Zone.lua +++ b/game/engines/default/engine/Zone.lua @@ -1003,7 +1003,7 @@ function _M:newLevel(level_data, lev, old_lev, game) local ux, uy, dx, dy, spots = generator:generate(lev, old_lev) if level.force_recreate then - forceprint("[Zone:newLevel] map generator"..generator.__CLASSNAME.." forced recreation: ",level.force_recreate) + forceprint("[Zone:newLevel] map generator "..generator.__CLASSNAME.." forced recreation: ",level.force_recreate) level:removed() return self:newLevel(level_data, lev, old_lev, game) end diff --git a/game/engines/default/engine/generator/map/Building.lua b/game/engines/default/engine/generator/map/Building.lua index 3e9ff6540a5605bcaa8e7d17ff0329cc4c8fd2a5..16000ccca4fa688a156fe6f38bb4f4e8b939d86f 100644 --- a/game/engines/default/engine/generator/map/Building.lua +++ b/game/engines/default/engine/generator/map/Building.lua @@ -26,20 +26,28 @@ local RoomsLoader = require "engine.generator.map.RoomsLoader" --- @classmod engine.generator.map.Building module(..., package.seeall, class.inherit(engine.Generator, RoomsLoader)) +--- populate the map with clusters of buildings ("blocks") divided into rooms ("buildings") by walls and corridors +-- special grids (in data): +-- external_floor = grid to use between buildings (rooms) +-- outside_floor = grid to use outside the building area function _M:init(zone, map, level, data) engine.Generator.init(self, zone, map, level) self.data = data self.grid_list = self.zone.grid_list - self.max_block_w = data.max_block_w or 20 - self.max_block_h = data.max_block_h or 20 - self.max_building_w = data.max_building_w or 7 - self.max_building_h = data.max_building_h or 7 - self.margin_w = data.margin_w or 0 - self.margin_h = data.margin_h or 0 - + self.max_block_w = data.max_block_w or 20 -- max width of a "block" (map partition) + self.max_block_h = data.max_block_h or 20 -- max height of a "block" (map partition) + self.max_building_w = data.max_building_w or 7 -- ~ max building width (width >= 2x this forces partitioning) + self.max_building_h = data.max_building_h or 7 -- ~ max building height (height >= 2x this forces partitioning) + self.margin_w = data.margin_w or 0 -- horizontal map border (outside of building area) + self.margin_h = data.margin_h or 0 -- vertical map border (outside of building area) + self.data.outside_floor = self.data.outside_floor or self.data.external_floor + self.max_room_w = data.max_room_w or (map.w - 2*self.margin_w)/2 - 1 -- maximum room width + self.max_room_h = data.max_room_h or (map.h - 2*self.margin_h)/2 - 1 -- maximum room height RoomsLoader.init(self, data) + end +--- place a door in a wall function _M:doorOnWall(wall) if wall.vert then local j = rng.table(table.keys(wall.ps)) @@ -51,23 +59,29 @@ function _M:doorOnWall(wall) wall.doored = true end +--- designate a line of grids as a wall and mark possible door spots function _M:addWall(vert, base, p1, p2) local walls = self.walls local ps = table.genrange(p1, p2, true) - local todel = {} for z, _ in pairs(ps) do local x, y if vert then x, y = base, z else x, y = z, base end - local nb = - (game.level.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) + - (game.level.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) + - (game.level.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) + - (game.level.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) - if nb ~= 2 then todel[#todel+1] = z end + local rm_map = self.map.room_map[x][y] + if rm_map.special or (rm_map.room and not rm_map.can_open) then + ps[z] = nil + else + rm_map.walled = true + if z == p1 or z == p2 or not game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") or 2 ~= + (game.level.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) + + (game.level.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) + + (game.level.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) + + (game.level.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) then + ps[z] = nil + end + end end - for i, z in ipairs(todel) do ps[z] = nil end - + -- remove duplicate door spots in any overlapping walls for i = 1, #walls do local w = walls[i] if w.vert == vert and w.base == base then @@ -76,78 +90,59 @@ function _M:addWall(vert, base, p1, p2) end walls[#walls+1] = {vert=vert, base=base, ps=ps} - +-- print(("\tAdding wall[%d]: %s:%d, (%d-%d)"):format(#walls, vert and "column" or "row", base, p1, p2)) return walls[#walls] end +--- create a "building" as an area within a "block" enclosed by walls function _M:building(leaf, spots) --- local x1, x2 = leaf.rx + rng.range(2, math.max(2, math.floor(leaf.w / 2 - 3))), leaf.rx + leaf.w - rng.range(2, math.max(2, math.floor(leaf.w / 2 - 3))) --- local y1, y2 = leaf.ry + rng.range(2, math.max(2, math.floor(leaf.h / 2 - 3))), leaf.ry + leaf.h - rng.range(2, math.max(2, math.floor(leaf.h / 2 - 3))) local x1, x2 = leaf.rx, leaf.rx + leaf.w local y1, y2 = leaf.ry, leaf.ry + leaf.h local ix1, ix2, iy1, iy2 = x1 + 2, x2 - 1, y1 + 2, y2 - 1 - local inner_grids = {} - local door_grids = {} local is_lit = rng.percent(self.data.lite_room_chance or 70) - for i = leaf.rx, leaf.rx + leaf.w do for j = leaf.ry, leaf.ry + leaf.h do - -- Abort if there is something already - if self.map:isBound(i, j) and self.map.room_map[i][j].room then return end - end end - + -- populate leaf grids for i = x1, x2 do for j = y1, y2 do - if i == x1 or i == x2 or j == y1 or j == y2 then - if not self.map.room_map[i][j].walled then self.map(i, j, Map.TERRAIN, self:resolve("wall")) end - self.map.room_map[i][j].walled = true - self.map.room_map[i][j].can_open = true - else - self.map(i, j, Map.TERRAIN, self:resolve("floor")) - if is_lit then self.map.lites(i, j, true) end - if i >= ix1 and i <= ix2 and j >= iy1 and j <= iy2 then - inner_grids[#inner_grids+1] = {x=i,y=j} + local rm_map = self.map.room_map[i][j] + if not rm_map.special and (not rm_map.room or rm_map.can_open) then + if i == x1 or i == x2 or j == y1 or j == y2 then + if not rm_map.walled then self.map(i, j, Map.TERRAIN, self:resolve("wall")) end + rm_map.walled = true + rm_map.can_open = true + elseif not rm_map.walled then + self.map(i, j, Map.TERRAIN, self:resolve("floor")) + if is_lit then self.map.lites(i, j, true) end end end end end - + local walls = {} - walls.up = self:addWall(false, y1, x1 + 1, x2 - 1) - walls.down = self:addWall(false, y2, x1 + 1, x2 - 1) - walls.left = self:addWall(true, x1, y1 + 1, y2 - 1) - walls.right = self:addWall(true, x2, y1 + 1, y2 - 1) + walls.up = self:addWall(false, y1, x1, x2) + walls.down = self:addWall(false, y2, x1, x2) + walls.left = self:addWall(true, x1, y1, y2) + walls.right = self:addWall(true, x2, y1, y2) self.rooms[#self.rooms+1] = {cx=math.floor((x1+x2)/2), cy=math.floor((y1+y2)/2), id=#self.rooms, walls=walls} - -- Eliminate inner grids that face the door - for i = #inner_grids, 1, -1 do - local g = inner_grids[i] - if door and (g.x == door.x or g.y == door.y) then table.remove(inner_grids, i) end - end - spots[#spots+1] = {x=math.floor((x1+x2)/2), y=math.floor((y1+y2)/2), type="building", subtype="building"} end +--- create a "block" as a contiguous partition of the map containing "buildings" function _M:block(leaf, spots) local x1, x2 = leaf.rx, leaf.rx + leaf.w local y1, y2 = leaf.ry, leaf.ry + leaf.h local ix1, ix2, iy1, iy2 = x1 + 2, x2 - 1, y1 + 2, y2 - 1 - local inner_grids = {} - local door_grids = {} - for i = leaf.rx, leaf.rx + leaf.w do for j = leaf.ry, leaf.ry + leaf.h do - -- Abort if there is something already - if self.map:isBound(i, j) and self.map.room_map[i][j].room then return end - end end - - local door_grids = {} + -- populate grids around the border for i = x1, x2 do for j = y1, y2 do - if i == x1 or i == x2 or j == y1 or j == y2 then - self.map(i, j, Map.TERRAIN, self:resolve("floor")) + if (i == x1 or i == x2 or j == y1 or j == y2) and not self.map.room_map[i][j].special and (not self.map.room_map[i][j].room or self.map.room_map[i][j].can_open) then + self.map(i, j, Map.TERRAIN, self:resolve("external_floor") or self:resolve("floor")) end end end local bsp = BSP.new(leaf.w-2, leaf.h-2, self.max_building_w, self.max_building_h) bsp:partition() - print("Building gen made ", #bsp.leafs, "building BSP leafs") + print("Building gen made ", #bsp.leafs, "buildings (BSP leafs)") for z, sleaf in ipairs(bsp.leafs) do sleaf.rx = sleaf.rx + leaf.rx + 1 sleaf.ry = sleaf.ry + leaf.ry + 1 @@ -157,22 +152,93 @@ end function _M:generate(lev, old_lev) for i = 0, self.map.w - 1 do for j = 0, self.map.h - 1 do - self.map(i, j, Map.TERRAIN, self:resolve("wall")) + if i <= self.margin_w - 1 or i >= self.map.w - self.margin_w + 0 or j <= self.margin_h - 1 or j >= self.map.h - self.margin_h + 0 then + self.map(i, j, Map.TERRAIN, self:resolve("outside_floor")) -- outside of building space + else + self.map(i, j, Map.TERRAIN, self:resolve("external_floor") or self:resolve("floor")) -- between 'buildings' + end end end - local spots = {} self.spots = spots self.walls = {} - self.rooms = {} - local bsp = BSP.new(self.map.w - self.margin_w, self.map.h - self.margin_h, self.max_block_w, self.max_block_h) - bsp:partition() + -- place specific rooms if needed + local nb_room = util.getval(self.data.nb_rooms or 0) + local rooms = self.map.room_map.rooms + + -- make sure rooms meet size limits and are placed inside the margins + local add_check = function(room, x, y) + local border = room.border or 0 + if room.w + 2*border > self.max_room_w or room.h > self.max_room_h + 2*border then + return false + end + if x <= self.margin_w + border or x + room.w + border >= self.map.w - self.margin_w or y <= self.margin_h + border or y + room.h + border >= self.map.h - self.margin_h then + return false + end + return true + end + + print("[Building] placing", nb_room, "rooms") + -- Place required rooms + if #self.required_rooms > 0 then + for i, rroom in ipairs(self.required_rooms) do + local ok = false + if type(rroom) == "table" and rroom.chance_room then + if rng.percent(rroom.chance_room) then rroom = rroom[1] ok = true end + else ok = true + end - print("Building gen made ", #bsp.leafs, "blocks BSP leafs") + if ok then + local r = self:roomAlloc(rroom, #rooms+1, lev, old_lev, add_check) + if r then nb_room = nb_room - 1 + else self.level.force_recreate = "required_room "..tostring(rroom) return end + end + end + end + + -- Place normal, random rooms + local tries = nb_room * 1.5 -- allow for extra attempts for difficult to place rooms + while tries > 0 and nb_room > 0 do + local rroom + while true do + rroom = self.rooms[rng.range(1, #self.rooms)] + print("[Building] picked random room", rroom) + if type(rroom) == "table" and rroom.chance_room then + if rng.percent(rroom.chance_room) then rroom = rroom[1] break end + else + break + end + end + + local r = self:roomAlloc(rroom, #rooms+1, lev, old_lev, add_check) + if r then nb_room = nb_room -1 end + tries = tries - 1 + end + + -- enable doors in edges of each room placed + for i, r in ipairs(rooms) do + if not r.room.no_tunnels then -- don't add doors to unconnected rooms + local walls = {} -- mark edges of room as walls + r.walls = walls + walls.top = self:addWall(false, r.y, r.x , r.x + r.room.w - 1) + walls.bottom = self:addWall(false, r.y + r.room.h - 1, r.x , r.x + r.room.w - 1) + walls.left = self:addWall(true, r.x, r.y, r.y + r.room.h - 1) + walls.right = self:addWall(true, r.x + r.room.w - 1, r.y, r.y + r.room.h - 1) + end + end + + -- partition the map into "blocks" + local bsp = BSP.new(self.map.w - 2*self.margin_w, self.map.h - 2*self.margin_h, self.max_block_w, self.max_block_h) + local store = {x=self.margin_w, y=self.margin_h, rx=self.margin_w, ry=self.margin_h, w=self.map.w - 2*self.margin_w - 1, h=self.map.h - 2*self.margin_h - 1, nodes={}, id=0, depth=0} + bsp:partition(store) + print("[Building] generator made ", #bsp.leafs, "blocks (BSP leafs)") + + -- resolve each "block" for z, leaf in ipairs(bsp.leafs) do self:block(leaf, spots) end - + + -- place a door in each designated wall for i = 1, #self.walls do self:doorOnWall(self.walls[i]) end @@ -187,13 +253,14 @@ function _M:generate(lev, old_lev) return ux, uy, dx, dy, spots end ---- Create the stairs inside the level +--- Create the stairs inside the level (inside the margin, if any) function _M:makeStairsInside(lev, old_lev, spots) + local m_w, m_h = self.margin_w, self.margin_h -- Put down stairs local dx, dy if lev < self.zone.max_level or self.data.force_last_stair then while true do - dx, dy = rng.range(1, self.map.w - 1), rng.range(1, self.map.h - 1) + dx, dy = rng.range(m_w + 1, self.map.w - m_w - 1), rng.range(m_h + 1, self.map.h - m_h - 1) if not self.map:checkEntity(dx, dy, Map.TERRAIN, "block_move") and not self.map.room_map[dx][dy].special then self.map(dx, dy, Map.TERRAIN, self:resolve("down")) self.map.room_map[dx][dy].special = "exit" @@ -205,7 +272,7 @@ function _M:makeStairsInside(lev, old_lev, spots) -- Put up stairs local ux, uy while true do - ux, uy = rng.range(1, self.map.w - 1), rng.range(1, self.map.h - 1) + ux, uy = rng.range(m_w + 1, self.map.w - m_w - 1), rng.range(m_h + 1, self.map.h - m_h - 1) if not self.map:checkEntity(ux, uy, Map.TERRAIN, "block_move") and not self.map.room_map[ux][uy].special then self.map(ux, uy, Map.TERRAIN, self:resolve("up")) self.map.room_map[ux][uy].special = "exit" @@ -216,16 +283,17 @@ function _M:makeStairsInside(lev, old_lev, spots) return ux, uy, dx, dy, spots end ---- Create the stairs on the sides +--- Create the stairs on the sides (inside the margin, if any) function _M:makeStairsSides(lev, old_lev, sides, spots) + local m_w, m_h = self.margin_w, self.margin_h -- Put down stairs local dx, dy if lev < self.zone.max_level or self.data.force_last_stair then while true do - if sides[2] == 4 then dx, dy = 0, rng.range(0, self.map.h - 1) - elseif sides[2] == 6 then dx, dy = self.map.w - 1, rng.range(0, self.map.h - 1) - elseif sides[2] == 8 then dx, dy = rng.range(0, self.map.w - 1), 0 - elseif sides[2] == 2 then dx, dy = rng.range(0, self.map.w - 1), self.map.h - 1 + if sides[2] == 4 then dx, dy = m_w, rng.range(m_h, self.map.h - m_h - 1) + elseif sides[2] == 6 then dx, dy = self.map.w - m_w - 1, rng.range(m_h, self.map.h - m_h - 1) + elseif sides[2] == 8 then dx, dy = rng.range(m_w, self.map.w - m_w - 1), m_h + elseif sides[2] == 2 then dx, dy = rng.range(m_w, self.map.w - m_w - 1), self.map.h- m_h - 1 end if not self.map.room_map[dx][dy].special then @@ -239,12 +307,12 @@ function _M:makeStairsSides(lev, old_lev, sides, spots) -- Put up stairs local ux, uy while true do - if sides[1] == 4 then ux, uy = 0, rng.range(0, self.map.h - 1) - elseif sides[1] == 6 then ux, uy = self.map.w - 1, rng.range(0, self.map.h - 1) - elseif sides[1] == 8 then ux, uy = rng.range(0, self.map.w - 1), 0 - elseif sides[1] == 2 then ux, uy = rng.range(0, self.map.w - 1), self.map.h - 1 + if sides[1] == 4 then ux, uy = m_w, rng.range(m_h, self.map.h - m_h - 1) + elseif sides[1] == 6 then ux, uy = self.map.w - m_w - 1, rng.range(m_h, self.map.h - m_h - 1) + elseif sides[1] == 8 then ux, uy = rng.range(m_w, self.map.w - m_w - 1), m_h + elseif sides[1] == 2 then ux, uy = rng.range(m_w, self.map.w - m_w - 1), self.map.h- m_h - 1 end - + if not self.map.room_map[ux][uy].special then self.map(ux, uy, Map.TERRAIN, self:resolve("up")) self.map.room_map[ux][uy].special = "exit" diff --git a/game/engines/default/engine/generator/map/RoomsLoader.lua b/game/engines/default/engine/generator/map/RoomsLoader.lua index bc36e9dd1fb431604926ca427dca5a1a2c9bc8a1..2558d12a9fca6f5b24eff60c6cd1d7ee5553d243 100644 --- a/game/engines/default/engine/generator/map/RoomsLoader.lua +++ b/game/engines/default/engine/generator/map/RoomsLoader.lua @@ -34,16 +34,20 @@ function _M:init(data) if data.rooms then for i, file in ipairs(data.rooms) do if type(file) == "table" then table.insert(self.rooms, {self:loadRoom(file[1]), chance_room=file[2]}) + print("[RoomsLoader:init] loaded room:", file[1], self.rooms[#self.rooms][1], "chance:", file[2]) else table.insert(self.rooms, self:loadRoom(file)) + print("[RoomsLoader:init] loaded room:", file, self.rooms[#self.rooms]) end end end if data.required_rooms then for i, file in ipairs(data.required_rooms) do if type(file) == "table" then table.insert(self.required_rooms, {self:loadRoom(file[1]), chance_room=file[2]}) + print("[RoomsLoader:init] loaded required room:", file[1], self.required_rooms[#self.required_rooms][1], "chance:", file[2]) else table.insert(self.required_rooms, self:loadRoom(file)) + print("[RoomsLoader:init] loaded required room:", file, self.required_rooms[#self.required_rooms]) end end end end @@ -564,7 +568,7 @@ function _M:roomCheck(room, zone, level, map) for i, xroom in ipairs(rooms_list) do if xroom.room then if xroom.room.unique == room.unique then - print("[roomCheck]-- rejecting duplicate of unique room", room.unique) + print("[RoomsLoader:roomCheck]-- rejecting duplicate of unique room", room.unique) return false, "unique:"..room.unique end end @@ -658,8 +662,7 @@ function _M:roomPlace(room, id, x, y) end end end - - print("room placed at", x, y,"with center",math.floor(x+(room.w-1)/2), math.floor(y+(room.h-1)/2)) + print("placed room", room.name ,"at", x, y,"with center",math.floor(x+(room.w-1)/2), math.floor(y+(room.h-1)/2)) cx = cx or math.floor(x+(room.w-1)/2) cy = cy or math.floor(y+(room.h-1)/2) local ret = { id=id, x=x, y=y, cx=cx, cy=cy, room=room } @@ -705,7 +708,7 @@ function _M:roomAlloc(room, id, lev, old_lev, add_check) if px and py then -- gradually spread additional tries around the preferred location local sig = tries/100 x = util.bound(rng.normal(px, sig*self.map.w*2), math.max(1, border), self.map.w - room.w - math.max(1, border)) - y = util.bound(rng.normal(px, sig*self.map.h*2), math.max(1, border), self.map.h - room.h - math.max(1, border)) + y = util.bound(rng.normal(py, sig*self.map.h*2), math.max(1, border), self.map.h - room.h - math.max(1, border)) else x = rng.range(math.max(1, border), self.map.w - room.w - math.max(1, border)) y = rng.range(math.max(1, border), self.map.h - room.h - math.max(1, border))