5365977789acf2771d45e8c606f827806da980be
[crawl.git] / crawl-ref / source / dat / dlua / ziggurat.lua
1 ------------------------------------------------------------------------------
2 -- ziggurat.lua:
3 --
4 -- Code for ziggurats.
5 --
6 -- Important notes:
7 -- ----------------
8 -- Functions that are attached to Lua markers' onclimb properties
9 -- cannot be closures, because Lua markers must be saved and closure
10 -- upvalues cannot (yet) be saved.
11 ------------------------------------------------------------------------------
12
13 function zig()
14   if not dgn.persist.ziggurat then
15     dgn.persist.ziggurat = { }
16     -- Initialise here to handle ziggurats accessed directly by &P.
17     initialise_ziggurat(dgn.persist.ziggurat)
18   end
19   return dgn.persist.ziggurat
20 end
21
22 local wall_colours = {
23   "blue", "red", "lightblue", "magenta", "green", "white"
24 }
25
26 function ziggurat_wall_colour()
27   return util.random_from(wall_colours)
28 end
29
30 function initialise_ziggurat(z, portal)
31   -- Any given ziggurat will use the same builder for all its levels,
32   -- and the same colours for outer walls. Before choosing the builder,
33   -- we specify a global excentricity. If zig_exc=0, then the ellipses
34   -- will be circles etc. It is not the actual excentricity but some
35   -- value between 0 and 100. For deformed ellipses and rectangles, make
36   -- sure that the map is wider than it is high for the sake of ASCII.
37
38   z.zig_exc = crawl.random2(101)
39   z.builder = ziggurat_choose_builder()
40   z.colour = ziggurat_wall_colour()
41 end
42
43 function callback.ziggurat_initialiser(portal)
44   dgn.persist.ziggurat = { }
45   initialise_ziggurat(dgn.persist.ziggurat, portal)
46 end
47
48 -- Common setup for ziggurat levels.
49 function ziggurat_level(e)
50   e.tags("allow_dup")
51   e.tags("no_dump")
52   e.orient("encompass")
53
54   if crawl.game_started() then
55     ziggurat_build_level(e)
56   end
57 end
58
59 -----------------------------------------------------------------------------
60 -- Ziggurat level builders.
61
62 beh_wander = mons.behaviour("wander")
63
64 function ziggurat_awaken_all(mons)
65   mons.beh = beh_wander
66 end
67
68 function ziggurat_build_level(e)
69   local builder = zig().builder
70
71   -- Deeper levels can have all monsters awake.
72   -- Does never happen at depths 1-4; does always happen at depths 25-27.
73   local generate_awake = you.depth() > 4 + crawl.random2(21)
74   zig().monster_hook = generate_awake and global_function("ziggurat_awaken_all")
75
76   -- Deeper levels may block controlled teleports.
77   -- Does never happen at depths 1-6; does always happen at depths 25-27.
78   if you.depth() > 6 + crawl.random2(19) then
79     dgn.change_level_flags("no_tele_control")
80   end
81
82   if builder then
83     return ziggurat_builder_map[builder](e)
84   end
85 end
86
87 local zigstair = dgn.gridmark
88
89 -- the estimated total map area for ziggurat maps of given depth
90 -- this is (almost) independent of the layout type
91 local function map_area()
92   return 30 + 18*you.depth() + you.depth()*you.depth()
93 end
94
95 local function clamp_in(val, low, high)
96   if val < low then
97     return low
98   elseif val > high then
99     return high
100   else
101     return val
102   end
103 end
104
105 local function clamp_in_bounds(x, y)
106   return clamp_in(x, 2, dgn.GXM - 3), clamp_in(y, 2, dgn.GYM - 3)
107 end
108
109 local function set_wall_tiles()
110   local tileset = {
111     blue      = "wall_zot_blue",
112     red       = "wall_zot_red",
113     lightblue = "wall_zot_cyan",
114     magenta   = "wall_zot_magenta",
115     green     = "wall_zot_green",
116     white     = "wall_vault"
117   }
118
119   local wall = tileset[zig().colour]
120   if (wall == nil) then
121     wall = "wall_vault"
122   end
123   dgn.change_rock_tile(wall)
124 end
125
126 local function with_props(spec, props)
127   local spec_table = type(spec) == "table" and spec or { spec = spec }
128   return util.cathash(spec_table, props)
129 end
130
131 local function spec_fn(specfn)
132   return { specfn = specfn }
133 end
134
135 local function spec_if(fn, spec)
136   local spec_table = type(spec) == "table" and spec or { spec = spec }
137   return util.cathash(spec_table, { cond = fn })
138 end
139
140 local function depth_ge(lev)
141   return function ()
142            return you.depth() >= lev
143          end
144 end
145
146 local function depth_range(low, high)
147   return function ()
148            return you.depth() >= low and you.depth() <= high
149          end
150 end
151
152 local function depth_lt(lev)
153   return function ()
154            return you.depth() < lev
155          end
156 end
157
158 local function zig_monster_fn(spec)
159   local mfn = dgn.monster_fn(spec)
160   return function (x, y)
161            local mons = mfn(x, y)
162            if mons then
163              local monster_hook = zig().monster_hook
164              if monster_hook then
165                monster_hook(mons)
166              end
167            end
168            return mons
169          end
170 end
171
172 local function monster_creator_fn(arg)
173   local atyp = type(arg)
174   if atyp == "string" then
175     local mcreator = zig_monster_fn(arg)
176
177     local function mspec(x, y, nth)
178       return mcreator(x, y)
179     end
180     return { fn = mspec, spec = arg }
181   elseif atyp == "table" then
182     if not arg.cond or arg.cond() then
183       local spec = arg.spec or arg.specfn()
184       return util.cathash(monster_creator_fn(spec), arg)
185     end
186   elseif atyp == "function" then
187     return { fn = arg }
188   end
189 end
190
191 local mons_populations = { }
192
193 local function mset(...)
194   util.foreach({ ... }, function (spec)
195                           table.insert(mons_populations, spec)
196                         end)
197 end
198
199 local function mset_if(condition, ...)
200   mset(unpack(util.map(util.curry(spec_if, condition), { ... })))
201 end
202
203 mset(with_props("place:Lair:$ w:165 / dire elephant w:12 / " ..
204                 "catoblepas w:12 / hellephant w:6 / spriggan druid w:1 / " ..
205                 "guardian serpent w:1 / deep troll shaman w:1 / " ..
206                 "raiju w:1 / hell beast w:1", { weight = 5 }),
207      with_props("place:Shoals:$ w:125 band / merfolk aquamancer / " ..
208                 "water nymph w:5 / merfolk impaler w:5 / " ..
209                 "merfolk javelineer / octopode crusher w:12", { weight = 5 }),
210      "place:Spider:$ w:115 / ghost moth w:15 / red wasp / " ..
211                 "orb spider",
212      "place:Crypt:$ 9 w:260 / curse skull w:5 / profane servitor w:5 / " ..
213                 "bone dragon / ancient lich / revenant",
214      "place:Abyss:$ w:1990 / corrupter",
215      with_props("place:Slime:$", { jelly_protect = true }),
216      with_props("place:Coc:$ w:460 / Ice Fiend / " ..
217                  "blizzard demon w:30", { weight = 5 }),
218      with_props("place:Geh:$ w:460 / Brimstone Fiend / " ..
219                  "balrug w:30", { weight = 5 }),
220      with_props("place:Dis:$ w:460 / Hell Sentinel / " ..
221                  "dancing weapon / war gargoyle w:20", { weight = 5 }),
222      with_props("place:Tar:$ w:460 / Shadow Fiend / " ..
223                  "curse toe / shadow demon w:20", { weight = 5 }),
224      with_props("daeva / angel / cherub / pearl dragon / " ..
225                 "ophan / apis", { weight = 2 }),
226      with_props("hill giant / cyclops / stone giant / fire giant / " ..
227                 "frost giant / ettin / titan", { weight = 2 }),
228      with_props("fire elemental / fire drake / hell hound / efreet / " ..
229                 "fire dragon / fire giant / orb of fire", { weight = 2 }),
230      with_props("ice beast / freezing wraith / ice dragon / " ..
231                 "frost giant / ice devil / ice fiend / simulacrum / " ..
232                 "white draconian knight / blizzard demon", { weight = 2 }),
233      with_props("insubstantial wisp / air elemental / titan / raiju / " ..
234                 "storm dragon / electric golem / spriggan air mage / " ..
235                 "shock serpent", { weight = 2 }),
236      with_props("gargoyle / earth elemental / torpor snail / boulder beetle / " ..
237                 "stone giant / iron dragon / crystal guardian / " ..
238                 "war gargoyle / iron golem / hell sentinel", { weight = 2 }),
239      with_props("spectral thing / shadow wraith / eidolon w:4 / " ..
240                 "shadow dragon / deep elf death mage w:6 / " ..
241                 "death knight w:4 / revenant w:4 / profane servitor w:6 / " ..
242                 "soul eater / shadow fiend / black sun", { weight = 2 }),
243      with_props("swamp drake / fire drake / wind drake w:2 / death drake / " ..
244                 "wyvern w:5 / hydra w:5 / steam dragon / mottled dragon / " ..
245                 "swamp dragon / fire dragon / ice dragon / storm dragon / " ..
246                 "iron dragon / shadow dragon / quicksilver dragon / " ..
247                 "golden dragon", { weight = 2 }),
248      with_props("centaur / yaktaur / centaur warrior / yaktaur captain / " ..
249                 "cyclops / stone giant / faun w:1 / satyr w:2 / " ..
250                 "thorn hunter w:2 / naga sharpshooter / " ..
251                 "merfolk javelineer / deep elf master archer", { weight = 2 }),
252      with_props("wizard / necromancer / ogre mage w:5 / orc sorcerer w:5 / " ..
253                 "naga mage / naga ritualist w:5 / salamander mystic w:5 / " ..
254                 "greater naga / tengu conjurer / tengu reaver / " ..
255                 "spriggan air mage w:5 / merfolk aquamancer w:5 / " ..
256                 "deep elf conjurer / deep elf annihilator / " ..
257                 "deep elf sorcerer / draconian scorcher w:5 / " ..
258                 "draconian knight w:5 / draconian annihilator w:5 / " ..
259                 "lich w:3 / ancient lich w:2 / blood saint", { weight = 2 }))
260
261 -- spec_fn can be used to wrap a function that returns a monster spec.
262 -- This is useful if you want to adjust monster weights in the spec
263 -- wrt to depth in the ziggurat.
264 mset(spec_fn(function ()
265                local d = 290 - 10 * you.depth()
266                local e = math.max(0, you.depth() - 20)
267                return "place:Orc:$ w:" .. d .. " / orc warlord / " ..
268                  "orc high priest band / orc sorcerer w:5 / stone giant / " ..
269                  "iron troll w:5 / moth of wrath w:" .. e
270              end))
271
272 mset(spec_fn(function ()
273                local d = 300 - 10 * you.depth()
274                return "place:Elf:$ w:" .. d .. " / deep elf high priest / " ..
275                  "deep elf blademaster / deep elf master archer / " ..
276                  "deep elf annihilator / deep elf demonologist"
277              end))
278
279 mset(spec_fn(function ()
280                local d = math.max(6, you.depth() * 2 - 2)
281                local e = math.max(1, you.depth() - 18)
282                return "place:Snake:$ w:130 / guardian serpent w:5 / " ..
283                  "greater naga w:" .. d .. " / quicksilver dragon w:" .. e
284              end))
285
286 mset(spec_fn(function ()
287                local d = math.max(120, 280 - 10 * you.depth())
288                local e = math.max(1, you.depth() - 9)
289                local f = math.max(1, math.floor((you.depth() - 9) / 3))
290                return "place:Swamp:$ w:" .. d .. " / eight-headed hydra w:8 / " ..
291                  "swamp dragon w:8 / tentacled monstrosity w:" .. e ..
292                  " / shambling mangrove w:" .. f .. " / green death w:6 / " ..
293                  "death drake w:1 / golden dragon w:1"
294              end))
295
296 mset(spec_fn(function ()
297                local d = math.max(1, you.depth() - 11)
298                return "place:Vaults:$ w:60 / place:Vaults:$ 9 w:30 / " ..
299                  "titan w:" .. d .. " / golden dragon w:" .. d ..
300                  " / ancient lich w:" .. d
301              end))
302
303 mset(spec_fn(function ()
304                local d = you.depth() + 5
305                return "place:Tomb:$ w:200 / greater mummy w:" .. d
306              end))
307
308 mset(spec_fn(function ()
309                local d = math.max(2, math.floor((32 - you.depth()) / 5))
310                local e = math.min(8, math.floor((you.depth()) / 5) + 4)
311                local f = math.max(1, you.depth() - 5)
312                return "chaos spawn w:" .. d .. " / ugly thing w:" .. d ..
313                  " / very ugly thing w:4 / apocalypse crab w:4 / " ..
314                  "shapeshifter hd:16 w:" ..e .. " / glowing shapeshifter w:" .. e ..
315                  " / killer klown w:8 / chaos champion w:2 / " ..
316                  "greater demon w:2 / pandemonium lord w:" .. f
317              end))
318
319 mset(spec_fn(function ()
320                local d = 41 - you.depth()
321                return "base draconian w:" .. d .. " / nonbase draconian w:40"
322              end))
323
324 local pan_lord_fn = zig_monster_fn("pandemonium lord")
325 local pan_critter_fn = zig_monster_fn("place:Pan / greater demon / nonbase demonspawn w:4")
326
327 local function mons_panlord_gen(x, y, nth)
328   if nth == 1 then
329     local d = math.max(1, you.depth() - 11)
330     dgn.set_random_mon_list("place:Pan / greater demon / nonbase demonspawn w:4")
331     return pan_lord_fn(x, y)
332   else
333     return pan_critter_fn(x, y)
334   end
335 end
336
337 mset_if(depth_ge(8), mons_panlord_gen)
338 mset_if(depth_ge(14), with_props("place:Snake:$ w:14 / place:Swamp:$ w:14 / " ..
339                       "place:Shoals:$ w:14 / place:Spider:$ w:14 / " ..
340                       "greater naga w:12 / guardian serpent w:8 / hydra w:5 / " ..
341                       "swamp dragon w:5 / tentacled monstrosity / " ..
342                       "merfolk aquamancer w:6 / merfolk javelineer w:8 / " ..
343                       "alligator snapping turtle w:6 / " ..
344                       "octopode crusher / ghost moth w:8 / " ..
345                       "emperor scorpion w:8 / moth of wrath w:4",
346                       { weight = 5 }))
347
348 function ziggurat_monster_creators()
349   return util.map(monster_creator_fn, mons_populations)
350 end
351
352 local function ziggurat_vet_monster(fmap)
353   local fn = fmap.fn
354   fmap.fn = function (x, y, nth, hdmax)
355               if nth >= dgn.MAX_MONSTERS then
356                 return nil
357               end
358               for i = 1, 10 do
359                 local mons = fn(x, y, nth)
360                 if mons then
361                   -- Discard zero-exp monsters, and monsters that explode
362                   -- the HD limit.
363                   if mons.experience == 0 or mons.hd > hdmax * 1.3 then
364                     mons.dismiss()
365                   else
366                     -- Monster is ok!
367                     return mons
368                   end
369                 end
370               end
371               -- Give up.
372               return nil
373             end
374   return fmap
375 end
376
377 local function choose_monster_set()
378   return ziggurat_vet_monster(
379            util.random_weighted_from(
380              'weight',
381              ziggurat_monster_creators()))
382 end
383
384 -- Function to find travel-safe squares, excluding closed doors.
385 local dgn_passable = dgn.passable_excluding("closed_door")
386
387 local function ziggurat_create_monsters(p, mfn)
388   local hd_pool = you.depth() * (you.depth() + 8) + 10
389
390   local nth = 1
391
392   local function mons_do_place(p)
393     if hd_pool > 0 then
394       local mons = mfn(p.x, p.y, nth, hd_pool)
395
396       if mons then
397         nth = nth + 1
398         hd_pool = hd_pool - mons.hd
399
400         if nth >= dgn.MAX_MONSTERS then
401           hd_pool = 0
402         end
403       else
404         -- Can't find any suitable monster for the HD we have left.
405         hd_pool = 0
406       end
407     end
408   end
409
410   local function mons_place(point)
411     if hd_pool <= 0 then
412       return true
413     elseif not dgn.mons_at(point.x, point.y) then
414       mons_do_place(point)
415     end
416   end
417
418   dgn.find_adjacent_point(p, mons_place, dgn_passable)
419 end
420
421 local function ziggurat_create_loot_at(c)
422   -- Basically, loot grows linearly with depth.
423   local depth = you.depth()
424   local nloot = depth
425   local nloot = depth + crawl.random2(math.floor(nloot * 0.5))
426
427   local function find_free_space(nspaces)
428     local spaces = { }
429     local function add_spaces(p)
430       if nspaces <= 0 then
431         return true
432       else
433         table.insert(spaces, p)
434         nspaces = nspaces - 1
435       end
436     end
437     dgn.find_adjacent_point(c, add_spaces, dgn_passable)
438     return spaces
439   end
440
441   -- dgn.good_scrolls is a list of items with total weight 1000
442   local good_loot = dgn.item_spec("* no_pickup no_mimic w:7000 / " .. dgn.good_scrolls)
443   local super_loot = dgn.item_spec("| no_pickup no_mimic w:7000 /" ..
444                                    "potion of experience no_pickup no_mimic w:200 /" ..
445                                    "potion of cure mutation no_pickup no_mimic w:200 /" ..
446                                    "potion of porridge no_pickup no_mimic w:100 /" ..
447                                    "wand of heal wounds no_pickup no_mimic w:10 / " ..
448                                    "wand of hasting no_pickup no_mimic w:10 / " ..
449                                    dgn.good_scrolls)
450
451   local loot_spots = find_free_space(nloot * 4)
452
453   if #loot_spots == 0 then
454     return
455   end
456
457   local curspot = 0
458   local function next_loot_spot()
459     curspot = curspot + 1
460     if curspot > #loot_spots then
461       curspot = 1
462     end
463     return loot_spots[curspot]
464   end
465
466   local function place_loot(what)
467     local p = next_loot_spot()
468     dgn.create_item(p.x, p.y, what)
469   end
470
471   for i = 1, nloot do
472     if crawl.one_chance_in(depth) then
473       for j = 1, 4 do
474         place_loot(good_loot)
475       end
476     else
477       place_loot(super_loot)
478     end
479   end
480 end
481
482 -- Suitable for use in loot vaults.
483 function ziggurat_loot_spot(e, key)
484   e.lua_marker(key, portal_desc { ziggurat_loot = "X" })
485   e.kfeat(key .. " = .")
486   e.marker("@ = lua:props_marker({ door_restrict=\"veto\" })")
487   e.kfeat("@ = +")
488 end
489
490 local has_loot_chamber = false
491
492 local function ziggurat_create_loot_vault(entry, exit)
493   local inc = (exit - entry):sgn()
494
495   local connect_point = exit - inc * 3
496   local map = dgn.map_by_tag("ziggurat_loot_chamber")
497
498   if not map then
499     return exit
500   end
501
502   local function place_loot_chamber()
503     local res = dgn.place_map(map, true, true)
504     if res then
505       has_loot_chamber = true
506     end
507     return res
508   end
509
510   local function good_loot_bounds(map, px, py, xs, ys)
511     local vc = dgn.point(px + math.floor(xs / 2),
512                          py + math.floor(ys / 2))
513
514
515     local function safe_area()
516       local p = dgn.point(px, py)
517       local sz = dgn.point(xs, ys)
518       local floor = dgn.fnum("floor")
519       return dgn.rectangle_forall(p, p + sz - 1,
520                                   function (c)
521                                     return dgn.grid(c.x, c.y) == floor
522                                   end)
523     end
524
525     local linc = (exit - vc):sgn()
526     -- The map's positions should be at the same increment to the exit
527     -- as the exit is to the entrance, else reject the place.
528     return (inc == linc) and safe_area()
529   end
530
531   local function connect_loot_chamber()
532     return dgn.with_map_bounds_fn(good_loot_bounds, place_loot_chamber)
533   end
534
535   local res = dgn.with_map_anchors(connect_point.x, connect_point.y,
536                                    connect_loot_chamber)
537   if not res then
538     return exit
539   else
540     -- Find the square to drop the loot.
541     local lootx, looty = dgn.find_marker_position_by_prop("ziggurat_loot")
542
543     if lootx and looty then
544       return dgn.point(lootx, looty)
545     else
546       return exit
547     end
548   end
549 end
550
551 local function ziggurat_locate_loot(entrance, exit, jelly_protect)
552   if jelly_protect then
553     return ziggurat_create_loot_vault(entrance, exit)
554   else
555     return exit
556   end
557 end
558
559 local function ziggurat_place_pillars(c)
560   local range = crawl.random_range
561   local floor = dgn.fnum("floor")
562
563   local map, vplace = dgn.resolve_map(dgn.map_by_tag("ziggurat_pillar"))
564
565   if not map then
566     return
567   end
568
569   local name = dgn.name(map)
570
571   local size = dgn.point(dgn.mapsize(map))
572
573   -- Does the pillar want to be centered?
574   local centered = string.find(dgn.tags(map), " centered ")
575
576   local function good_place(p)
577     local function good_square(where)
578       return dgn.grid(where.x, where.y) == floor
579     end
580     return dgn.rectangle_forall(p, p + size - 1, good_square)
581   end
582
583   local function place_pillar()
584     if centered then
585       if good_place(c) then
586         return dgn.place_map(map, true, true, c.x, c.y)
587       end
588     else
589       for i = 1, 100 do
590         local offset = range(-15, -size.x)
591         local offsets = {
592           dgn.point(offset, offset) - size + 1,
593           dgn.point(offset - size.x + 1, -offset),
594           dgn.point(-offset, -offset),
595           dgn.point(-offset, offset - size.y + 1)
596         }
597
598         offsets = util.map(function (o)
599                              return o + c
600                            end, offsets)
601
602         if util.forall(offsets, good_place) then
603           local function replace(at, hflip, vflip)
604             dgn.reuse_map(vplace, at.x, at.y, hflip, vflip)
605           end
606
607           replace(offsets[1], false, false)
608           replace(offsets[2], false, true)
609           replace(offsets[3], true, false)
610           replace(offsets[4], false, true)
611           return true
612         end
613       end
614     end
615   end
616
617   for i = 1, 5 do
618     if place_pillar() then
619       break
620     end
621   end
622 end
623
624 local function ziggurat_stairs(entry, exit)
625   zigstair(entry.x, entry.y, "stone_arch", "stone_stairs_up_i")
626
627   if you.depth() < dgn.br_depth(you.branch()) then
628     zigstair(exit.x, exit.y, "stone_stairs_down_i")
629   end
630
631   zigstair(exit.x, exit.y + 1, "exit_ziggurat")
632   zigstair(exit.x, exit.y - 1, "exit_ziggurat")
633 end
634
635 local function ziggurat_furnish(centre, entry, exit)
636   has_loot_chamber = false
637   local monster_generation = choose_monster_set()
638
639   if type(monster_generation.spec) == "string" then
640     dgn.set_random_mon_list(monster_generation.spec)
641   end
642
643   -- Identify where we're going to place loot, but don't actually put
644   -- anything down until we've placed pillars.
645   local lootspot = ziggurat_locate_loot(entry, exit,
646     monster_generation.jelly_protect)
647
648   if not has_loot_chamber then
649     -- Place pillars if we did not create a loot chamber.
650     ziggurat_place_pillars(centre)
651   end
652
653   ziggurat_create_loot_at(lootspot)
654
655   ziggurat_create_monsters(exit, monster_generation.fn)
656
657   local function needs_colour(p)
658     return not dgn.in_vault(p.x, p.y)
659       and dgn.grid(p.x, p.y) == dgn.fnum("permarock_wall")
660   end
661
662   dgn.colour_map(needs_colour, zig().colour)
663   set_wall_tiles()
664 end
665
666 -- builds ziggurat maps consisting of two overimposed rectangles
667 local function ziggurat_rectangle_builder(e)
668   local grid = dgn.grid
669   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
670
671   local area = map_area()
672   area = math.floor(area*3/4)
673
674   local cx, cy = dgn.GXM / 2, dgn.GYM / 2
675
676   -- exc is the local eccentricity for the two rectangles
677   -- exc grows with depth as 0-1, 1, 1-2, 2, 2-3 ...
678   local exc = math.floor(you.depth() / 2)
679   if ((you.depth()-1) % 2) == 0 and crawl.coinflip() then
680     exc = exc + 1
681   end
682
683   local a = math.floor(math.sqrt(area+4*exc*exc))
684   local b = a - 2*exc
685   local a2 = math.floor(a / 2) + (a % 2)
686   local b2 = math.floor(b / 2) + (b % 2)
687   local x1, y1 = clamp_in_bounds(cx - a2, cy - b2)
688   local x2, y2 = clamp_in_bounds(cx + a2, cy + b2)
689   dgn.fill_grd_area(x1, y1, x2, y2, "floor")
690
691   local zig_exc = zig().zig_exc
692   local nx1 = cx + y1 - cy
693   local ny1 = cy + x1 - cx + math.floor(you.depth()/2*(200-zig_exc)/300)
694   local nx2 = cx + y2 - cy
695   local ny2 = cy + x2 - cx - math.floor(you.depth()/2*(200-zig_exc)/300)
696   nx1, ny1 = clamp_in_bounds(nx1, ny1)
697   nx2, ny2 = clamp_in_bounds(nx2, ny2)
698   dgn.fill_grd_area(nx1, ny1, nx2, ny2, "floor")
699
700   local entry = dgn.point(x1, cy)
701   local exit = dgn.point(x2, cy)
702
703   if you.depth() % 2 == 0 then
704     entry, exit = exit, entry
705   end
706
707   ziggurat_stairs(entry, exit)
708   ziggurat_furnish(dgn.point(cx, cy), entry, exit)
709 end
710
711 -- builds elliptic ziggurat maps
712 -- given the area, half axes a and b are determined by:
713 -- pi*a*b=area,
714 -- a=b for zig_exc=0,
715 -- a=b*3/2 for zig_exc=100
716 local function ziggurat_ellipse_builder(e)
717   local grid = dgn.grid
718   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
719
720   local zig_exc = zig().zig_exc
721
722   local area = map_area()
723   local b = math.floor(math.sqrt(200*area/(200+zig_exc) * 100/314))
724   local a = math.floor(b * (200+zig_exc) / 200)
725   local cx, cy = dgn.GXM / 2, dgn.GYM / 2
726
727   local floor = dgn.fnum("floor")
728
729   for x=0, dgn.GXM-1 do
730     for y=0, dgn.GYM-1 do
731       if b*b*(cx-x)*(cx-x) + a*a*(cy-y)*(cy-y) <= a*a*b*b then
732         grid(x, y, floor)
733       end
734     end
735   end
736
737   local entry = dgn.point(cx-a+2, cy)
738   local exit  = dgn.point(cx+a-2, cy)
739
740   if you.depth() % 2 == 0 then
741     entry, exit = exit, entry
742   end
743
744   ziggurat_stairs(entry, exit)
745   ziggurat_furnish(dgn.point(cx, cy), entry, exit)
746 end
747
748
749 -- builds hexagonal ziggurat maps
750 local function ziggurat_hexagon_builder(e)
751   local grid = dgn.grid
752   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
753
754   local zig_exc = zig().zig_exc
755
756   local c = dgn.point(dgn.GXM, dgn.GYM) / 2
757   local area = map_area()
758
759   local a = math.floor(math.sqrt(2 * area / math.sqrt(27))) + 2
760   local b = math.floor(a*math.sqrt(3)/4)
761
762   local left = dgn.point(math.floor(c.x - (a + math.sqrt(2 * a)) / 2),
763                          c.y)
764   local right = dgn.point(2 * c.x - left.x, c.y)
765
766   local floor = dgn.fnum("floor")
767
768   for x = 1, dgn.GXM - 2 do
769     for y = 1, dgn.GYM - 2 do
770       local dlx = x - left.x
771       local drx = x - right.x
772       local dly = y - left.y
773       local dry = y - right.y
774
775       if dlx >= dly and drx <= dry
776         and dlx >= -dly and drx <= -dry
777         and y >= c.y - b and y <= c.y + b then
778         grid(x, y, floor)
779       end
780     end
781   end
782
783   local entry = left + dgn.point(1,0)
784   local exit  = right - dgn.point(1, 0)
785
786   if you.depth() % 2 == 0 then
787     entry, exit = exit, entry
788   end
789
790   ziggurat_stairs(entry, exit)
791   ziggurat_furnish(c, entry, exit)
792 end
793
794 ----------------------------------------------------------------------
795
796 ziggurat_builder_map = {
797   rectangle = ziggurat_rectangle_builder,
798   ellipse = ziggurat_ellipse_builder,
799   hex = ziggurat_hexagon_builder
800 }
801
802 local ziggurat_builders = util.keys(ziggurat_builder_map)
803
804 function ziggurat_choose_builder()
805   return util.random_from(ziggurat_builders)
806 end