1------------------------------------------------------------------------------ 2-- v_rooms.lua: 3-- 4-- Functions to code-generate some common standard types of room. 5------------------------------------------------------------------------------ 6 7hyper.rooms = {} 8 9-- Picks a generator from a weighted table of generators and builds a room with it 10function hyper.rooms.pick_room(build,options) 11 12 if hyper.profile then 13 profiler.push("PickRoom") 14 end 15 16 -- This callback filters out generators based on standard options which we support for all generators 17 local function weight_callback(generator) 18 -- max_rooms controls how many rooms _this_ generator can place before it stops 19 if generator.max_rooms ~= nil and generator.placed_count ~= nil and generator.placed_count >= generator.max_rooms then 20 return 0 21 end 22 -- min_total_rooms means this many rooms have to have been placed by _any_ generator before this one comes online 23 if generator.min_total_rooms ~= nil and options.rooms_placed < generator.min_total_rooms then return 0 end 24 -- max_total_rooms stops this generator after this many rooms have been placed by any generators 25 if generator.max_total_rooms ~= nil and options.rooms_placed >= generator.max_total_rooms then return 0 end 26 -- Generator can have a weight function instead of a flat weight 27 if generator.weight_callback ~= nil then return generator.weight_callback(generator,options) end 28 -- Otherwise use the generator's default weight 29 return generator.weight 30 end 31 32 local weights = build.generators or options.room_type_weights 33 34 -- Pick generator from weighted table 35 local chosen = util.random_weighted_from(weight_callback,weights) 36 local room 37 38 -- Main generator loop 39 local veto,tries,maxTries = false,0,50 40 while tries < maxTries and (room == nil or veto) do 41 tries = tries + 1 42 veto = false 43 44 room = hyper.rooms.make_room(options,chosen) 45 46 -- Allow veto callback to throw this room out (e.g. on size, exits) 47 -- TODO: This veto is still unused and I don't have a use case, maybe should delete it. 48 if options.veto_room_callback ~= nil then 49 veto = options.veto_room_callback(room) 50 end 51 52 end 53 54 if hyper.profile then 55 profiler.pop({ tries = tries }) 56 end 57 58 return room 59end 60 61-- Makes a room object from a generator table. Optionally perform analysis 62-- (default true) to determine open and connectable squares. 63function hyper.rooms.make_room(options,generator) 64 65 if hyper.profile then 66 profiler.push("MakeRoom") 67 end 68 69 -- Code rooms 70 if generator.generator == "code" then 71 room = hyper.rooms.make_code_room(generator,options) 72 73 -- Pick vault map by tag 74 elseif generator.generator == "tagged" then 75 room = hyper.rooms.make_tagged_room(generator,options) 76 end 77 78 if hyper.profile then 79 profiler.pop() 80 profiler.push("AnalyseRoom") 81 end 82 83 if room == nil then return nil end 84 85 -- Do we analyse this room? Usually necessary but we might not need 86 -- it e.g. random placement layouts where it doesn't matter so much 87 local analyse = true 88 if generator.analyse ~= nil then analyse = generator.analyse end 89 90 if analyse then 91 hyper.rooms.analyse_room(room,options) 92 end 93 94 if hyper.profile then 95 profiler.pop() 96 profiler.push("TransformRoom") 97 end 98 99 -- Optionally transform the room. This is used to add walls for V 100 -- and other layouts but a transform doesn't have to be provided. 101 local transform = generator.room_transform or options.room_transform 102 if transform ~= nil then 103 room = transform(room,options) 104 if perform_analysis then 105 hyper.rooms.analyse_room(room,options) 106 end 107 end 108 109 if hyper.profile then 110 profiler.pop() 111 profiler.push("AnalyseRoomInternal") 112 end 113 114 if generator.analyse_interal then 115 hyper.usage.analyse_grid_usage(room.grid,options) 116 end 117 118 if hyper.profile then 119 profiler.pop() 120 end 121 122 if hyper.debug then 123 dump_usage_grid_v3(room.grid) 124 end 125 126 -- Assign the room an id. This is useful later on when we carve doors etc. so we can identify which walls belong to which rooms. 127 -- TODO: The id could instead be incremented for each attempt at placing a room. We can hold onto rooms that failed to place and try 128 -- them again later on in case space has opened up. Not sure if this is a good idea but it could save some cycles since 129 -- generating rooms is one of the more expensive operations now. Really before making any assumptions like that I should run 130 -- some proper analysis of the expensiveness of different operations and get a good picture of what needs to be optimised. 131 -- Perhaps it's still possible to find some ways to move expensive parts out to C++ (e.g. mask and normal generation, placement itself) 132 -- whilst retaining callbacks. 133 room.id = options.rooms_placed 134 135 return room 136end 137 138function hyper.rooms.make_code_room(chosen,options) 139 -- Pick a size for the room 140 local size 141 if chosen.size ~= nil then 142 if type(chosen.size) == "function" then 143 -- Callback function 144 size = chosen.size(options,chosen) 145 else 146 -- Static size 147 size = chosen.size 148 end 149 else 150 -- Default size function 151 local size_func = chosen.size_callback or hyper.rooms.size_default 152 size = size_func(chosen,options) 153 end 154 155 room = { 156 type = "grid", 157 size = size, 158 generator_used = chosen, 159 grid = hyper.usage.new_usage(size.x,size.y) 160 } 161 162 -- Paint and decorate the layout grid 163 hyper.paint.paint_grid(chosen.paint_callback(room,options,chosen),options,room.grid) 164 if chosen.decorate_callback ~= nil then chosen.decorate_callback(room.grid,room,options) end -- Post-production 165 166 return room 167end 168 169-- Pick a map by tag and make a room grid from it 170function hyper.rooms.make_tagged_room(chosen,options) 171 -- Resolve a map with the specified tag 172 local mapdef = dgn.map_by_tag(chosen.tag,true) 173 if mapdef == nil then return nil end -- Shouldn't happen when thing are working but just in case 174 175 -- Temporarily prevent map getting mirrored / rotated during resolution because it'll seriously 176 -- screw with our attempts to understand and position the vault later; and hardwire transparency because lack of it can fail a whole layout 177 -- TODO: Store these tags on the room first so we can actually support them down the line ... 178 dgn.tags(mapdef, "no_vmirror no_hmirror no_rotate transparent"); 179 -- Resolve the map so we can find its width / height 180 local map, vplace = dgn.resolve_map(mapdef,false) 181 local room,room_width,room_height 182 183 -- If we can't find a map then we're probably not going to find one 184 if map == nil then return nil end 185 -- Allow map to be flipped and rotated again, otherwise we'll struggle later when we want to rotate it into the correct orientation 186 dgn.tags_remove(map, "no_vmirror no_hmirror no_rotate") 187 188 local room_width,room_height = dgn.mapsize(map) 189 local veto = false 190 -- Check min/max room sizes are observed 191 if (chosen.min_size == nil or not (room_width < chosen.min_size or room_height < chosen.min_size)) 192 and (chosen.max_size == nil or not (room_width > chosen.max_size or room_height > chosen.max_size)) then 193 194 room = { 195 type = "vault", 196 size = { x = room_width, y = room_height }, 197 map = map, 198 vplace = vplace, 199 generator_used = chosen, 200 grid = hyper.usage.new_usage(room_width,room_height) 201 } 202 203 room.preserve_wall = dgn.has_tag(room.map, "preserve_wall") 204 room.no_windows = dgn.has_tag(room.map, "no_windows") 205 206 -- Check all four directions for orient tag before we create the wals data, since the existence of a 207 -- single orient tag makes the other walls ineligible 208 room.tags = {} 209 for n = 0, 3, 1 do 210 if dgn.has_tag(room.map,"vaults_orient_" .. vector.normals[n+1].name) then 211 room.has_orient = true 212 room.tags[n] = true 213 end 214 end 215 -- Make usage grid by feature inspection; will be used to compute wall data 216 for m = 0, room.size.y - 1, 1 do 217 for n = 0, room.size.x - 1, 1 do 218 local inspected = { } 219 inspected.feature, inspected.exit, inspected.space = dgn.inspect_map(vplace,n,m) 220 inspected.solid = not (feat.has_solid_floor(feature) or feat.is_door(feature)) 221 inspected.feature = dgn.feature_name(inspected.feature) 222 hyper.usage.set_usage(room.grid,n,m,inspected) 223 end 224 end 225 found = true 226 end 227 return room 228end 229 230-- Analyse the exit squares of a room to determine connectability / eligibility for placement. 231function hyper.rooms.analyse_room(room,options) 232 -- Initialise walls table 233 room.walls = { } 234 for n = 0, 3, 1 do 235 room.walls[n] = { eligible = false } 236 end 237 238 local inspect_cells = {} 239 local has_exits = false 240 241 if hyper.profile then 242 profiler.push("FirstPass") 243 end 244 245 for m = 0, room.size.y-1, 1 do 246 for n = 0, room.size.x-1, 1 do 247 local cell = hyper.usage.get_usage(room.grid,n,m) 248 249 -- If we find an exit then flag this, causing non-exit edge cells to get ignored 250 if cell.exit then has_exits = true end 251 cell.anchors = {} 252 -- Remember if the cell is a relevant connector cell 253 if not cell.space and (cell.carvable or not cell.solid) then 254 table.insert(inspect_cells,{ cell = cell, pos = { x = n, y = m } } ) 255 end 256 end 257 end 258 259 if hyper.profile then 260 profiler.pop() 261 profiler.push("SecondPass") 262 end 263 264 -- Loop through the cells again now we know has_exits, and store all connected cells in our list of walls for later. 265 for i,inspect in ipairs(inspect_cells) do 266 local cell = inspect.cell 267 local n,m = inspect.pos.x,inspect.pos.y 268 269 if not has_exits or cell.exit then 270 -- Analyse squares around it 271 for i,normal in ipairs(room.allow_diagonals and vector.directions or vector.normals) do 272 local near_pos = { x = n + normal.x,y = m + normal.y } 273 local near_cell = hyper.usage.get_usage(room.grid,near_pos.x,near_pos.y) 274 if near_cell == nil or near_cell.space then 275 -- This is a possible anchor 276 local anchor = true 277 local target = inspect.pos 278 local anchor_pos = normal 279 -- If it's a wall we need space on the opposite side 280 if cell.solid then 281 anchor = false 282 if cell.carvable then 283 local target_cell = hyper.usage.get_usage(room.grid,normal.x - n,normal.y - m) 284 if target_cell ~= nil and not target_cell.solid then 285 anchor_pos = { x = 0, y = 0 } 286 anchor = true 287 end 288 end 289 end 290 if anchor then 291 -- Record this is as a possible anchor 292 table.insert(cell.anchors, { normal = normal, pos = anchor_pos, origin = inspect.pos }) 293 -- table.insert(room.walls[normal.dir],{ cell = cell, pos = inspect.pos, normal = normal } ) 294 -- room.walls[normal.dir].eligible = true 295 cell.connected = true 296 end 297 end 298 end 299 hyper.usage.set_usage(room.grid,n,m,cell) 300 end 301 end 302 303 if hyper.profile then 304 profiler.pop() 305 end 306end 307 308-- Transforms a room by returning a new room sized 2 bigger with walls added next to all open edge squares 309function hyper.rooms.add_walls(room, options) 310 311 local walled_room = { 312 type = "transform", 313 size = { x = room.size.x + 2, y = room.size.y + 2 }, 314 generator_used = room.generator_used, 315 transform = "add_walls", 316 flags = room.flags, 317 inner_room = room, 318 inner_room_pos = { x = 1, y = 1 } 319 } 320 321 walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y) 322 -- Loop through all the squares and add the walls 323 for m = 0, walled_room.size.y-1, 1 do 324 for n = 0, walled_room.size.x-1, 1 do 325 -- Get corresponding square from original room 326 local usage = hyper.usage.get_usage(room.grid,n-1,m-1) 327 if usage == nil then usage = { space = true } end 328 -- If it's not space, copy usage from the inner room 329 if not usage.space then 330 usage.inner = true 331 hyper.usage.set_usage(walled_room.grid,n,m,usage) 332 else 333 -- Otherwise check if we're bordering any open squares and therefore need to draw a wall. 334 local any_open = false 335 for i,normal in ipairs(vector.directions) do 336 local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1) 337 if near_cell == nil then near_cell = { space = true } end 338 if not near_cell.space and not near_cell.solid then 339 any_open = true 340 break 341 end 342 end 343 if any_open then 344 -- There was at least one open square so we need to make a wall which *could* be carvable. 345 -- Note: carvable doesn't mean this can necessarily be used as a door, e.g. Fort crennelations ... that will still happen in room analysys. It 346 -- makes the logic overall much simpler. 347 -- TODO: Allow diagonal doors here too. Diagonals are problematic (in the previous loop and in analyse_room) because we'd get overlapping anchors 348 -- with adjacent walls, this really doesn't sound good. 349 local wall_usage = { feature = "rock_wall", wall = true, protect = true } 350 for i,normal in ipairs(vector.normals) do 351 local near = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1) 352 if near ~= nil and near.connected then 353 wall_usage.carvable = true 354 wall_usage.connected = true -- TODO: Might be redundant 355 end 356 end 357 hyper.usage.set_usage(walled_room.grid, n, m, wall_usage) 358 end 359 end 360 end 361 end 362 363 return walled_room 364end 365 366function hyper.rooms.add_buffer(room, options) 367 368 local walled_room = { 369 type = "transform", 370 size = { x = room.size.x + 2, y = room.size.y + 2 }, 371 generator_used = room.generator_used, 372 transform = "add_buffer", 373 flags = room.flags, 374 } 375 376 -- Take on the existing inner room if available (wall+buffer) 377 walled_room.inner_room = room.inner_room or room 378 walled_room.inner_room_pos = room.inner_room_pos and { x = room.inner_room_pos.x+1, y = room.inner_room_pos.y+1 } or { x = 1, y = 1 } 379 walled_room.grid = hyper.usage.new_usage(walled_room.size.x,walled_room.size.y) 380 381 -- Loop through all the squares and add the buffer 382 for m = 0, walled_room.size.y-1, 1 do 383 for n = 0, walled_room.size.x-1, 1 do 384 -- Get corresponding square from original room 385 local usage = hyper.usage.get_usage(room.grid,n-1,m-1) 386 if usage == nil then usage = { space = true } end 387 -- If it's not space, copy usage from the inner room 388 if not usage.space then 389 usage.inner = true 390 hyper.usage.set_usage(walled_room.grid,n,m,usage) 391 else 392 -- Otherwise check if we're bordering any non-space and therefore need to create a buffer 393 local any_open = false 394 for i,normal in ipairs(vector.directions) do 395 local near_cell = hyper.usage.get_usage(room.grid,n+normal.x-1,m+normal.y-1) 396 if near_cell == nil then near_cell = { space = true } end 397 if not near_cell.space then 398 any_open = true 399 break 400 end 401 end 402 if any_open then 403 hyper.usage.set_usage(walled_room.grid, n, m, { space = true, buffer = true }) 404 end 405 end 406 end 407 end 408 409 return walled_room 410end 411 412function hyper.rooms.add_buffer_walls(room, options) 413 return hyper.rooms.add_buffer(hyper.rooms.add_walls(room,options),options) 414end 415