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