1############################################################################### 2# layout_halls.des: Hall-like formations featuring rooms and corridors 3############################################################################### 4 5: crawl_require("dlua/util.lua") 6: crawl_require("dlua/layout/procedural.lua") 7: crawl_require("dlua/layout/procedural_primitives.lua") 8: crawl_require("dlua/layout/procedural_complex.lua") 9: crawl_require("dlua/layout/procedural_transform.lua") 10: crawl_require("dlua/layout/zonify.lua") 11: crawl_require("dlua/layout/theme.lua") 12: crawl_require("dlua/layout/minimum_map_area.lua") 13 14{{ 15 16 local onion_weights = { 17 -- TODO: More shapes are possible; triangle, oval, rectangle, elongated circle, 18 -- actually a generic polygon covers the triangle case, maybe irregular polygons, 19 -- stars, 20 { weight = 10, func = function() return primitive.distance end }, 21 { weight = 10, func = function() return primitive.box end }, 22 { weight = 10, func = function() return primitive.diamond() end }, 23 { weight = 10, func = function() return primitive.octagon(0.25) end }, 24 { weight = 10, func = function() return primitive.hexagon(0.25) end }, 25 -- ex did produce some interesting stuff but it's not ideal 26 -- { weight = 10, func = function() return primitive.ex() end }, 27 } 28 29 -- Generates the main onion function 30 function onion_skin(num_rings,wall_break) 31 local chosen = util.random_weighted_from("weight",onion_weights) 32 local fcircle = chosen.func() 33 34 -- Make holes in each partitioning wall 35 local rings = {} 36 local hole_size = util.random_range_real(0.1,0.3) -- 0.6 37 38 -- TODO: There are some other functions that could be used to carve holes; e.g. spirals, worley ... 39 local phasering = function(size) 40 local num = crawl.random_range(5*size,20*size) + 1 41 return procedural.phase(primitive.radial,crawl.random_range(2,10),crawl.random_real()) 42 end 43 44 local randomring = function(size) 45 local p = 0 46 end 47 48 for n=1,num_rings-1,1 do 49 rings[n] = phasering(n/num_rings) 50 end 51 52 -- Do we create a central room? 53 local fill_center = crawl.coinflip() 54 55 local fphase = function(x,y) 56 local r = fcircle(x,y) 57 if r >= 1 then return -1 end 58 local rr = r * num_rings 59 local level = math.floor(rr) 60 r = rr - level 61 if fill_center and level == 0 then return 1 end 62 63 -- Fill connecting walls? 64 if level > 0 and r <= wall_break then 65 local hole = rings[level] 66 if hole ~= nil then 67 return hole(x,y) 68 end 69 end 70 71 return r 72 end 73 74 return fphase 75 end 76 77}} 78 79############################################################## 80# layout_hall_layers 81# 82# TODO: Correctly assign "layout_type_open_caves" tag with cave 83# mode and "layout_mode_passages" tag without it. 84# TODO: Needs to make narrower paths (e.g. max 5 width). When 85# that works, add to Zot at weight 15. 86# TODO: Elf needs a version that never does caves. When that 87# and previous TODO are in, add to Elf with weight 10. 88# 89NAME: layout_hall_layers 90DEPTH: Lair 91WEIGHT: 7 92ORIENT: encompass 93TAGS: overwritable layout allow_dup unrand central layout_type_narrow_caves 94{{ 95 if is_validating() then return; end 96 local gxm,gym = dgn.max_bounds() 97 98 -- Actual corridor width will be double this; it gets increased later 99 -- but it needs to be declared here so it's in scope for the callbacks 100 local width = 1 101 102 -- TODO: 103 -- * More interesting primary shape options 104 -- * One or two more layers would be nice, or separating things into separate 105 -- groups of stuff since there are a lot of shapes here and they don't always work well together 106 -- * Make some layers optional 107 -- * Negative space 108 -- * Invert floor and use interference between layers 109 110 -- A big shape that should span the entire level to make sure everything is connected 111 local shapes1 = { 112 { weight = 10, func = primitive.cross }, 113 { weight = 3, func = primitive.ex }, 114 { weight = 10, func = function() return procedural.mul(crawl.random_range(4,8),procedural.sub(primitive.distance,primitive.box)) end }, 115 -- Rays 116 { weight = 5, func = function() return procedural.mul(width*8,procedural.phase(primitive.radial, crawl.random_range(4,8), crawl.random_real())) end }, 117 } 118 119 -- A medium sized shape that circles the level 120 local shapes2 = { 121 { weight = 10, func = function() return primitive.ring(gym/3) end }, 122 { weight = 10, func = function() return primitive.ringify(primitive.box,gym/3) end }, 123 { weight = 10, func = function() return primitive.ringify(primitive.diamond(),2/5*gym) end }, 124 { weight = 10, func = function() return primitive.ringify(primitive.octagon(crawl.random_range(5,20)),gym/3) end }, 125 { weight = 10, func = function() return primitive.ringify(primitive.hexagon(crawl.random_range(5,15)),gym/3) end }, 126 { weight = 10, func = function() return procedural.mul(1.8,primitive.ringify(procedural.scale(primitive.triangle(),1,2),gym/6)) end }, 127 { weight = 10, func = function() return procedural.mul(width, procedural.map(procedural.scale(complex.cog( 128 crawl.random_range(10,30), -- Teeth 129 util.random_range_real(0.7,0.9), -- Inner distance 130 util.random_range_real(0.3,0.6), -- Gap between teeth 131 util.random_range_real(0,1) -- Offset (TODO: Fix how the procedural works so we can align at 0 or 0.5 for symmettry) 132 ),gym*4/9),{ { 0,0.9,2,2 }, { 0.9,1,0.9,1 } })) end }, 133 } 134 135 -- Smaller shapes to fit in the middle 136 function scale_or_ring(prim, size) 137 if crawl.one_chance_in(3) then 138 -- Larger central shape 139 return procedural.scale(prim,2) 140 elseif crawl.coinflip() then 141 -- Small ring 142 return primitive.ringify(prim,size/2) 143 elseif crawl.one_chance_in(8) then 144 -- Very rarely don't actually scale it - it's quite interesting when 145 -- e.g. circle and diamond overlap at the same size. 146 return primitive.ringify(prim,size) 147 else 148 -- Small ring w/narrower corridors 149 return procedural.scale(primitive.ringify(prim,size),.5) 150 end 151 end 152 153 local shapes3 = { 154 { weight = 10, func = function() return scale_or_ring(primitive.distance,gym/3) end }, 155 { weight = 10, func = function() return scale_or_ring(primitive.box,gym/3) end }, 156 { weight = 10, func = function() return scale_or_ring(primitive.octagon(crawl.random_range(4,8)),gym/3) end }, 157 { weight = 10, func = function() return scale_or_ring(primitive.hexagon(crawl.random_range(4,8)),gym/3) end }, 158 { weight = 10, func = function() return scale_or_ring(procedural.scale(primitive.triangle(),1,2),gym/6) end }, 159 { weight = 10, func = function() return scale_or_ring(primitive.diamond(),2/5*gym) end }, 160 { weight = 10, func = function() return procedural.scale(primitive.ex(),.5) end }, 161 { weight = 10, func = function() return primitive.ringify(primitive.ex(),crawl.random_range(gym/6,gym/4)) end }, 162 { weight = 10, func = function() return primitive.ringify(primitive.cross(),crawl.random_range(gym/6,gym/4)) end }, 163 { weight = 10, func = function() return procedural.mul(crawl.random_range(2,4),primitive.ringify(procedural.sub(primitive.distance,primitive.box),6)) end }, 164 { weight = 10, func = function() return procedural.mul(width, procedural.scale(complex.cog( 165 crawl.random_range(3,8)*2, -- Teeth 166 util.random_range_real(0.1,0.9), -- Inner distance 167 util.random_range_real(0.4,0.6), -- Gap between teeth 168 util.random_range_real(0,1) -- Offset (TODO: Fix how the procedural works so we can align at 0 or 0.5 for symmettry) 169 ),gym/4)) end }, 170 -- Spiral -- this didn't work too well as a primary function, but it looks awesome 171 -- when overlaid with a second-level gear. It'd be nice to make it able to overlay 172 -- with both gears on those extremely rare occasions. 173 -- TODO: Actually there should be a completely separate layout with spiral + cogs, maybe Dis 174 { weight = 10, func = function() 175 local rot = crawl.random_range(5,20) 176 return function(x,y) 177 local r = primitive.radial(x,y) 178 local d = primitive.distance(x,y) 179 if d > gym/2 then return width end 180 return (r + d/rot)%1 * width * rot * .4 181 end 182 end 183 }, 184 } 185 186 local cave = you.in_branch("D") and crawl.coinflip() 187 or you.in_branch("Lair") and crawl.x_chance_in_y(2,3) 188 189 if cave then 190 width = crawl.random_range(4,6) 191 else 192 width = crawl.random_range(2,3) 193 end 194 195 local s1 = util.random_weighted_from("weight",shapes1) 196 local s2 = util.random_weighted_from("weight",shapes2) 197 local s3 = util.random_weighted_from("weight",shapes3) 198 199 local func = procedural.translate( 200 procedural.min( 201 s1.func(), 202 s2.func(), 203 s3.func() 204 ), 205 gxm/2,gym/2 206 ) 207 208 func = procedural.map(func,width/2,width,0,1) 209 210 if cave then 211 func = procedural.add(func,procedural.worley_diff{scale=.5}) 212 end 213 214 -- TODO: Package the func(s) into room generators. Then place them using hyper (with a 215 -- symmetrically padded placement strategy). Then optionally outline with transparent 216 -- stone and surround in lava like old crosses layout 217 218 procedural.render_map(_G, func, function(v) return (v < 1 and '.' or 'x') end) 219 zonify.map_fill_zones(_G, 1, 'x') 220 221 if cave then theme.D.caves(_G) 222 else theme.level_material(_G) end 223}} 224MAP 225ENDMAP 226 227############################################################## 228# layout_onion 229# 230# Takes one of the primitive shapes and creates nested corridors 231# in that shape, somewhat like an onion. Carves randomly spaced 232# gaps in the walls to connect everything up. 233# 234NAME: layout_onion 235DEPTH: Snake, Zot 236WEIGHT: 10 (Snake), 5 (Zot) 237ORIENT: encompass 238TAGS: overwritable layout allow_dup unrand central layout_type_narrow_caves 239{{ 240 if is_validating() then return; end 241 local gxm,gym = dgn.max_bounds() 242 243 -- TODO: 244 -- * Implement chaotic gaps sizing 245 -- * Tune params a bit for Snake 246 247 local wall_break = util.random_range_real(.4,.7) 248 249 -- Make the rings 250 local num_rings = crawl.random2avg(7,2)+2 251 local fphase = onion_skin(num_rings,wall_break) 252 253 -- Position on map 254 fphase = procedural.translate(procedural.scale(fphase,math.min(gxm,gym)/2-2),gxm/2,gym/2) 255 256 if you.in_branch("Snake") then 257 fphase = procedural.transform.damped_distortion(fphase) 258 end 259 260 procedural.render_map(_G, fphase, function(v) return (v > wall_break and '.' or 'x') end) 261 zonify.map_fill_zones(_G, 1, 'x') 262 263 theme.level_material(_G) 264}} 265MAP 266ENDMAP 267 268############################################################## 269# layout_catacombs 270# 271# Takes an inverted version of the onion layout so what were 272# walls are now rooms - connects these up with a noise pattern. 273# Hopefully this should look like ancient abandoned catacombs. 274# 275# TODO: Other ways of connecting up the rooms. Need something 276# starting in the centre and spreading out in a branching 277# way, like particle diffusion. 278# 279NAME: layout_catacombs 280DEPTH: Crypt, !Crypt:$, Tar 281WEIGHT: 10 (Crypt), 10 (Tar) 282ORIENT: encompass 283TAGS: overwritable layout allow_dup unrand layout_type_open_caves 284{{ 285 if is_validating() then return; end 286 local gxm,gym = dgn.max_bounds() 287 288 -- Make the rings 289 local num_rings = crawl.random2avg(4,2)+4 290 local wall_break = util.random_range_real(0.4,0.7) 291 local fphase = onion_skin(num_rings,wall_break) 292 293 -- Position on map 294 fphase = procedural.translate(procedural.scale(fphase,math.min(gxm,gym)/(crawl.random2(3)/2+1)), 295 gxm/2,gym/2) 296 297 -- NOTE: The following didn't quite work but was pretty cool. Attempted to 'erode' the 298 -- level by drawing noise layers over it. It mostly managed to connect everything but 299 -- was just a bit too messy. 300 --local fconnect = procedural.sub(1, 301 -- procedural.abs(procedural.simplex3d { scale = 4, unit = false }), 302 -- procedural.abs(procedural.simplex3d { scale = 4, unit = false }) 303 --) 304 305 local use_water = you.in_branch("crypt") and crawl.one_chance_in(4) 306 307 -- Pick a connector function to link up the rooms 308 local fconnect 309 if crawl.coinflip() then 310 fconnect = complex.rays(crawl.random_range(5,10), crawl.random_real(), util.random_range_real(30,35), util.random_range_real(0.1,0.2)) 311 -- Distort more the further out the rays go 312 local fdsx = procedural.simplex3d { scale = util.random_range_real(.5,1), unit = false } 313 local fdsy = procedural.simplex3d { scale = util.random_range_real(.5,1), unit = false } 314 local fedge = procedural.min(1,procedural.div(primitive.distance,math.min(gxm,gym)/2)) 315 local fdx = procedural.mul(fdsx,fedge) 316 local fdy = procedural.mul(fdsy,fedge) 317 fconnect = procedural.distort { source = fconnect, scale = crawl.random_range(10,20), offsetx = fdx, offsety = fdy } 318 fconnect = procedural.translate(fconnect,gxm/2,gym/2) 319 use_water = false 320 else 321 fconnect = procedural.abs(procedural.simplex3d { scale = util.random_range_real(0.6,0.8), unit = false }) 322 end 323 324 local connect_glyph = '.' 325 local water_glyph = 'W' 326 327 -- Do the render 328 procedural.render_map(_G, fphase, function(v,x,y) 329 local vconn = fconnect(x,y) 330 if vconn < 0.1 then 331 if use_water and (v == -1 or vconn < 0.06) then 332 return water_glyph 333 elseif v > -1 then 334 return connect_glyph 335 end 336 end 337 return (v < wall_break and v > -1 and '.' or 'x') 338 end) 339 340 zonify.map_fill_zones(_G, 1, 'x') 341 theme.level_material(_G) 342}} 343# Enforce minimum floor size - otherwise we get very tiny floors sometimes 344validate {{ 345 -- If we try to validate the map before without creating it, 346 -- we get a crash at start 347 if is_validating() then return end 348 return minimum_map_area.is_map_big_enough(_G, minimum_map_area.OPEN_CAVES, ".W") 349}} 350 351############################################################## 352# layout_onion_interference 353# 354# Layers two onions on top of each other to create an 355# interference pattern (with option xor) 356# 357# TODO: A better way of dealing with small maps 358# 359NAME: layout_onion_interference 360DEPTH: Snake, Zot:1-4 361WEIGHT: 10 362ORIENT: encompass 363TAGS: overwritable layout allow_dup unrand central layout_type_narrow_caves 364{{ 365 366 local gxm,gym = dgn.max_bounds() 367 368 local wall_break = util.random_range_real(.4,.6) 369 local num_rings1 = crawl.random_range(5,8) 370 local num_rings2 = crawl.random_range(5,8) 371 372 local fill_center = crawl.coinflip() 373 local frings1 = onion_skin(num_rings1,wall_break) 374 local frings2 = onion_skin(num_rings2,wall_break) 375 376 local dist = crawl.random_range(3,6) 377 local dir = { x = util.random_range_real(0.5,1) * dist, 378 y = util.random_range_real(0.5,1) * dist } 379 380 -- Position on map 381 frings1 = procedural.translate( 382 procedural.scale(frings1, 383 math.min(gxm,gym)*(3+crawl.random2(2))/8), 384 gxm/2,gym/2) 385 frings2 = procedural.translate( 386 procedural.scale(frings2, 387 math.min(gxm,gym)*(3+crawl.random2(2))/8), 388 gxm/2+dir.x,gym/2+dir.y) 389 390 if you.in_branch("Snake") then 391 frings1 = procedural.transform.damped_distortion(frings1) 392 frings2 = procedural.transform.damped_distortion(frings2) 393 end 394 395 -- Generate the interference pattern by combining the two functions 396 local xor = crawl.coinflip() 397 local fphase = function(x,y) 398 if xor then 399 if frings1(x, y) > wall_break then 400 return frings2(x,y) > wall_break and 0 or 1 401 else 402 return frings2(x,y) > wall_break and 1 or 0 403 end 404 end 405 return (frings1(x,y) + frings2(x,y)) / 2 406 end 407 408 procedural.render_map(_G, fphase, function(v) return (v > wall_break and '.' or 'x') end) 409 410 zonify.map_fill_zones(_G, 1, 'x') 411}} 412validate {{ 413 return minimum_map_area.is_map_big_enough(_G, minimum_map_area.NARROW_CAVES) 414}} 415 416############################################################## 417# layout_cathedral_of_symmetry 418# 419# This maps the x,y domain to a 3D cylinder extruded along the 420# y-axis in the Worley space. This creates a strong sense of 421# repetition and symmetry along the horizontal whilst still 422# allowing the patterns to connect. Optionally it applies a 423# polar transform producing something sometimes like a 424# spiderweb. 425# 426# TODO: Use "layout_type_narrow_caves" tag for the less open 427# (normal) version and the "layout_type_open_caves" tag 428# for the openm version that sometimes appears. If they 429# are all mixed up, layout_type_open_caves is probably 430# better. 431# TODO: less open space on Zot 432# 433NAME: layout_cathedral_of_symmetry 434DEPTH: Zot, Tar 435WEIGHT: 10 436ORIENT: encompass 437TAGS: overwritable layout allow_dup unrand central layout_type_open_caves 438{{ 439 if is_validating() then return; end 440 local gxm,gym = dgn.max_bounds() 441 442 -- zscale controls the symmetry within each repetition because with 0 443 -- the coord bounces back and forth along a line instead of tracing 444 -- a circle. Increasing towards 1 results in less and less symmetry until 445 -- we get a standard worley pattern at 1. 446 local zscale = 0 447 448 -- zoff gradually increases the z-axis offset as we move along x. 449 -- A non-zero value results in each repetition looking slightly different. 450 local zoff = 0 451 452 -- How many times to loop the pattern. Use multiples of 0.5 to 453 -- take advantage of the symmetrical nature of the algorithm 454 local reps = (crawl.random2(8)+1)/2 455 456 -- Offsets the whole pattern by a given multiple of repetitions. 457 -- This means the large symmetrical parts can be in the middle or on 458 -- the edge. Using multiples of 0.25 as these create a distinct geometry. 459 local xoff = crawl.random2(4)/4 460 461 -- Note: varying the scales is very tricky. At higher reps we can't get 462 -- away with so much variance. 463 -- Vertical axis scale 464 local yscale = 1 465 -- Scale of the underlying worley 466 local wscale = .2 467 468 local border = crawl.random_range(3,6) 469 local margin = crawl.random_range(0,5) 470 471 local polarise = false 472 473 if you.in_branch("tar") then 474 -- Tar works best with a lot of dissonance but occasionally 475 -- repeating elements 476 zscale = util.random_range_real(0.2,0.6) 477 zoff = util.random_range_real(0.1,0.3) 478 yscale = util.random_range_real(.4,1)/reps + reps/10 479 wscale = util.random_range_real(.06, .1) 480 polarise = crawl.one_chance_in(4) 481 else 482 -- Zot:5 483 if you.at_branch_bottom() then 484 reps = crawl.random2(4)+0.5 485 xoff = 0 486 border = 6 487 margin = 8 488 end 489 490 -- In Zot we want a strong set of symmetry and repetition 491 zscale = util.random_range_real(0,0.05) 492 zoff = util.random_range_real(0,0.25) 493 yscale = util.random_range_real(.1,.4)/reps + reps/10 494 wscale = util.random_range_real(.05, .08) 495 polarise = crawl.coinflip() 496 end 497 498 -- If this is being placed to benefit a central vault, do the polar layout. 499 local params = dgn.map_parameters() 500 if params ~= nil then 501 local mode = unpack(params) 502 if mode == "central" then 503 polarise = true 504 end 505 end 506 507 -- Zot:5 always gets a polar layout. 508 if you.in_branch("zot") and you.at_branch_bottom() then 509 polarise = true 510 end 511 512 local scale = 80/reps 513 local breakpoint = 0.3 514 local fbase = procedural.worley_diff{ scale = wscale } 515 fbase = procedural.transform.wrapped_cylinder(fbase,scale,scale,zscale,zoff) 516 fbase = procedural.scale(fbase,1,yscale) 517 fbase = procedural.translate(fbase,scale*xoff/2,0) 518 -- Perform polar transform. 519 -- TODO: On certain settings this starts looking like a giant spiderweb. Should enable this 520 -- layout in Spider with tuned settings and polarise = true 521 if polarise then 522 fbase = procedural.translate(procedural.transform.polar(fbase,gym/2,gxm,gym),gxm/2,gym/2) 523 end 524 fbase = procedural.add(fbase,procedural.mul(breakpoint,procedural.border{padding=border,margin=margin})) 525 526 procedural.render_map(_G, fbase, function(v) return (v < breakpoint and '.' or 'x') end) 527 zonify.map_fill_zones(_G, 1, 'x') 528 theme.level_material(_G) 529}} 530MAP 531ENDMAP 532 533############################################################## 534# layout_concentric_octagons 535# Idea by Vivificient, code by infiniplex 536# 537# This is properly called a labyrinth, but that name is already taken 538# 539# TODO: Minimum area validation in Snake (can be disconnected). 540# Then remove minimum size validate as unneeded 541# 542NAME: layout_concentric_octagons 543DEPTH: Lair, Snake, Zot 544WEIGHT: 3 (Lair), 5 (Snake), 10 (Zot) 545ORIENT: encompass 546TAGS: overwritable layout allow_dup unrand central layout_type_passages 547TAGS: no_rotate no_vmirror no_hmirror 548{{ 549 -- xx 550 -- xxxx 551 -- xxxx 552 -- xx 553 function draw_plug (x, y, fill) 554 for x1 = x - 1, x + 2 do 555 for y1 = y - 1, y + 2 do 556 if ( (x1 ~= x - 1 and x1 ~= x + 2) 557 or (y1 ~= y - 1 and y1 ~= y + 2)) then 558 mapgrd[x1][y1] = fill 559 end 560 end 561 end 562 end 563 function is_plug (x, y, check) 564 for x1 = x - 1, x + 2 do 565 for y1 = y - 1, y + 2 do 566 if ( (x1 ~= x - 1 and x1 ~= x + 2) 567 or (y1 ~= y - 1 and y1 ~= y + 2)) then 568 if (mapgrd[x1][y1] == check) then 569 return true 570 end 571 end 572 end 573 end 574 return false 575 end 576 577 function sign (n) 578 if (n > 0) then 579 return 1 580 elseif (n < 0) then 581 return -1 582 else 583 return 0 584 end 585 end 586 587 function resolve_position (layer, position, out_straight, out_diagonal) 588 local x 589 local y 590 if (position < layer.side_start[1]) then 591 -- north 592 local offset = position - layer.side_start[0] 593 x = layer.x2 + offset 594 y = layer.y1 - out_straight 595 elseif (position < layer.side_start[2]) then 596 -- northeast 597 local offset = position - layer.side_start[1] 598 out_diagonal = math.floor(out_diagonal) 599 x = layer.x3 + offset + out_diagonal 600 y = layer.y1 + offset - out_diagonal + 1 601 elseif (position < layer.side_start[3]) then 602 -- east 603 local offset = position - layer.side_start[2] 604 x = layer.x4 + out_straight - 1 605 y = layer.y2 + offset 606 elseif (position < layer.side_start[4]) then 607 -- southeast 608 local offset = position - layer.side_start[3] 609 out_diagonal = math.floor(out_diagonal) 610 x = layer.x4 - offset + out_diagonal - 1 611 y = layer.y3 + offset + out_diagonal 612 elseif (position < layer.side_start[5]) then 613 -- south 614 local offset = position - layer.side_start[4] 615 x = layer.x3 - offset - 1 616 y = layer.y4 + out_straight - 1 617 elseif (position < layer.side_start[6]) then 618 -- southwest 619 local offset = position - layer.side_start[5] 620 out_diagonal = math.floor(out_diagonal) 621 x = layer.x2 - offset - out_diagonal 622 y = layer.y4 - offset + out_diagonal - 1 623 elseif (position < layer.side_start[7]) then 624 -- west 625 local offset = position - layer.side_start[6] 626 x = layer.x1 - out_straight 627 y = layer.y3 - offset - 1 628 else 629 -- northwest 630 local offset = position - layer.side_start[7] 631 out_diagonal = math.ceil(out_diagonal) -- otherwise blocks miss hallway 632 x = layer.x1 + offset - out_diagonal + 1 633 y = layer.y2 - offset - out_diagonal 634 end 635 return x, y 636 end 637 638 -- draw hallways of the correct width, length, and direction 639 function draw_hallway_s_n (x, y, length, fill) 640 local sign_val = sign(length) 641 for y1 = y, y + length, sign(length) do 642 mapgrd[x ][y1] = fill 643 mapgrd[x + 1][y1] = fill 644 end 645 end 646 function draw_hallway_e_w (x, y, length, fill) 647 local sign_val = sign(length) 648 for x1 = x, x + length, sign(length) do 649 mapgrd[x1][y ] = fill 650 mapgrd[x1][y + 1] = fill 651 end 652 end 653 function draw_hallway_se_nw (x, y, length, fill) 654 local sign_val = sign(length) 655 for i = 0, length, sign_val do 656 local x1 = x + i 657 local y1 = y + i 658 mapgrd[x1][y1] = fill 659 if (i ~= 0) then 660 mapgrd[x1 - sign_val][y1 ] = fill 661 mapgrd[x1 ][y1 - sign_val] = fill 662 end 663 end 664 end 665 function draw_hallway_sw_ne (x, y, length, fill) 666 local sign_val = sign(length) 667 for i = 0, length, sign_val do 668 local x1 = x - i 669 local y1 = y + i 670 mapgrd[x1][y1] = fill 671 if (i ~= 0) then 672 mapgrd[x1 + sign_val][y1 ] = fill 673 mapgrd[x1 ][y1 - sign_val] = fill 674 end 675 end 676 end 677 678 local GXM, GYM = dgn.max_bounds() 679 extend_map{width = GXM, height = GYM} 680 fill_area{fill="x"} 681 682 local CENTER_X = GXM / 2 683 local CENTER_Y = GYM / 2 684 685 local HALLWAY_SIZE = you.in_branch("Zot") and 2 or 3 686 local layer_size = 2 * crawl.random_range(HALLWAY_SIZE, HALLWAY_SIZE + 2) 687 local center_radius = 2 * crawl.random_range(2, 3) 688 local layer_size_diagonal = layer_size * 3 / 4 -- can have a .5 on it 689 690 local border = crawl.random_range(2, 8 - (layer_size - 4)) 691 local max_radius = math.min(CENTER_X, CENTER_Y) - border 692 local layer_count = math.floor((max_radius - center_radius) / layer_size) 693 694 local radial_separation_min = crawl.random_range(4, 8) 695 local radial_separation_max = crawl.random_range(radial_separation_min+4, 20) 696 local plug_percent = crawl.random_range(0, 100) 697 698 -- make the octagon layers 699 local layer = {} 700 for i = layer_count, 0, -1 do 701 -- calculate octagon properties 702 -- 703 -- .... y1 704 -- ...... 705 -- ... ... y2 706 -- .. .. 707 -- .. .. 708 -- ... ... y3 709 -- ...... 710 -- .... y4 711 -- 712 layer[i] = {} 713 layer[i].radius = center_radius + layer_size * i 714 layer[i].oblique = layer[i].radius / 2 715 layer[i].x1 = CENTER_X - layer[i].radius 716 layer[i].x4 = CENTER_X + layer[i].radius - 1 717 layer[i].x2 = layer[i].x1 + layer[i].oblique 718 layer[i].x3 = layer[i].x4 - layer[i].oblique 719 layer[i].y1 = CENTER_Y - layer[i].radius 720 layer[i].y4 = CENTER_Y + layer[i].radius - 1 721 layer[i].y2 = layer[i].y1 + layer[i].oblique 722 layer[i].y3 = layer[i].y4 - layer[i].oblique 723 724 -- where each of the 8 sides start 725 -- -> needed for placing connections between rings 726 layer[i].side_straight = layer[i].x3 - layer[i].x2 - 1 727 layer[i].side_diagonal = layer[i].x4 - layer[i].x3 - 1 728 layer[i].side_start = {} 729 layer[i].side_start[0] = 0 730 for j = 1, 8 do 731 if (j % 2 == 0) then 732 layer[i].side_start[j]=layer[i].side_start[j-1]+layer[i].side_diagonal 733 else 734 layer[i].side_start[j]=layer[i].side_start[j-1]+layer[i].side_straight 735 end 736 end 737 738 -- draw the octagons 739 -- -> inner room has chance of being open 740 octa_room{ x1 = layer[i].x1, y1 = layer[i].y1, 741 x2 = layer[i].x4, y2 = layer[i].y4, 742 oblique = layer[i].oblique, 743 replace = 'x', inside = '.', outside = 'x' } 744 745 if (i > 0 or (crawl.one_chance_in(3) and layer[i].radius >= 3)) then 746 octa_room{ x1 = layer[i].x1 + HALLWAY_SIZE, 747 y1 = layer[i].y1 + HALLWAY_SIZE, 748 x2 = layer[i].x4 - HALLWAY_SIZE, 749 y2 = layer[i].y4 - HALLWAY_SIZE, 750 oblique = layer[i].oblique - HALLWAY_SIZE / 2, 751 replace = '.', inside = 'x', outside = '.' } 752 end 753 end 754 755 -- connect the layers 756 for i = 0, layer_count - 1 do 757 local total_distance = layer[i].side_start[8] 758 local hallway_pos = crawl.random2(total_distance) 759 local hallway_pos_end = hallway_pos + total_distance-radial_separation_min 760 local is_next_plug = false 761 762 while (hallway_pos < hallway_pos_end) do 763 -- find a spot for the hallway 764 local hallway_pos_fixed = hallway_pos % total_distance 765 local good_spot = false 766 while (not good_spot and hallway_pos < hallway_pos_end) do 767 local x, y = resolve_position(layer[i], hallway_pos_fixed, 0, 0) 768 if (not is_plug(x, y, 'c')) then 769 good_spot = true 770 else 771 hallway_pos = hallway_pos + crawl.random_range(1, 4) 772 hallway_pos_fixed = hallway_pos % total_distance 773 end 774 end 775 776 if (good_spot) then 777 if (is_next_plug) then 778 -- draw a plug 779 local x, y = resolve_position(layer[i], hallway_pos_fixed, 780 layer_size, layer_size_diagonal) 781 is_next_plug = false -- 2 blocks in a row can disconnect map 782 783 draw_plug(x, y, 'c') 784 else 785 -- draw a hallway 786 local x, y = resolve_position(layer[i], hallway_pos_fixed, 0, 0) 787 is_next_plug = crawl.x_chance_in_y(plug_percent, 100) 788 789 if (hallway_pos_fixed < layer[i].side_start[1]) then 790 draw_hallway_s_n (x, y, -layer_size, '.') -- north 791 elseif (hallway_pos_fixed < layer[i].side_start[2]) then 792 draw_hallway_sw_ne(x, y, -layer_size_diagonal, '.') -- northeast 793 elseif (hallway_pos_fixed < layer[i].side_start[3]) then 794 draw_hallway_e_w (x, y, layer_size, '.') -- east 795 elseif (hallway_pos_fixed < layer[i].side_start[4]) then 796 draw_hallway_se_nw(x, y, layer_size_diagonal, '.') -- southeast 797 elseif (hallway_pos_fixed < layer[i].side_start[5]) then 798 draw_hallway_s_n (x, y, layer_size, '.') -- south 799 elseif (hallway_pos_fixed < layer[i].side_start[6]) then 800 draw_hallway_sw_ne(x, y, layer_size_diagonal, '.') -- southwest 801 elseif (hallway_pos_fixed < layer[i].side_start[7]) then 802 draw_hallway_e_w (x, y, -layer_size, '.') -- west 803 else 804 draw_hallway_se_nw(x, y, -layer_size_diagonal, '.') -- northwest 805 end 806 end 807 end 808 809 hallway_pos = hallway_pos + crawl.random_range(radial_separation_min, 810 radial_separation_max) 811 end 812 end 813 814 -- turn the plugs into ordinary walls and floor 815 subst("c = x") 816 817 -- Smear the layout a la layout_big_octagon outside Zot. 818 -- (Actually, that's where this code originates.) 819 if not you.in_branch("Zot") then 820 local gxm, gym = dgn.max_bounds() 821 local iterations = 50 + crawl.random2(50) + crawl.random2(50) 822 smear_map{iterations = iterations, boxy = false} 823 824 mapgrd[gxm/2][gym/2] = '@' 825 fill_disconnected{wanted = '@'} 826 mapgrd[gxm/2][gym/2] = '.' 827 end 828}} 829# Enforce minimum floor size - remove when connection is working in Snake 830validate {{ 831 return minimum_map_area.is_map_big_enough(_G, minimum_map_area.PASSAGES) 832}} 833