Skip to content
Snippets Groups Projects
Commit 196e703a authored by DarkGod's avatar DarkGod
Browse files

Merge branch 'BuildingMapUpdate' into 'master'

Building map generator update

* The Building map generator can now accommodate rooms.

Rooms are (depending on their design) seamlessly integrated with the BSP-based automatic buildings.
The data.margin_w, data.margin_h parameters are correctly respected, and grids outside of the building area can be specified.

Fixed a bug in RoomsLoader that was incorrectly applying the results of room.prefer_location functions and added some informative print statements when loading room files.



See merge request !348
parents 19b34cc1 e66f9cb2
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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"
......
......@@ -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))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment