1 ------------------------------------------------------------------------------
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 ------------------------------------------------------------------------------
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)
19 return dgn.persist.ziggurat
22 local wall_colours = {
23 "blue", "red", "lightblue", "magenta", "green", "white"
26 function ziggurat_wall_colour()
27 return util.random_from(wall_colours)
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.
38 z.zig_exc = crawl.random2(101)
39 z.builder = ziggurat_choose_builder()
40 z.colour = ziggurat_wall_colour()
43 function callback.ziggurat_initialiser(portal)
44 dgn.persist.ziggurat = { }
45 initialise_ziggurat(dgn.persist.ziggurat, portal)
48 -- Common setup for ziggurat levels.
49 function ziggurat_level(e)
54 if crawl.game_started() then
55 ziggurat_build_level(e)
59 -----------------------------------------------------------------------------
60 -- Ziggurat level builders.
62 beh_wander = mons.behaviour("wander")
64 function ziggurat_awaken_all(mons)
68 function ziggurat_build_level(e)
69 local builder = zig().builder
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")
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")
83 return ziggurat_builder_map[builder](e)
87 local zigstair = dgn.gridmark
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()
95 local function clamp_in(val, low, high)
98 elseif val > high then
105 local function clamp_in_bounds(x, y)
106 return clamp_in(x, 2, dgn.GXM - 3), clamp_in(y, 2, dgn.GYM - 3)
109 local function set_wall_tiles()
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",
119 local wall = tileset[zig().colour]
120 if (wall == nil) then
123 dgn.change_rock_tile(wall)
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)
131 local function spec_fn(specfn)
132 return { specfn = specfn }
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 })
140 local function depth_ge(lev)
142 return you.depth() >= lev
146 local function depth_range(low, high)
148 return you.depth() >= low and you.depth() <= high
152 local function depth_lt(lev)
154 return you.depth() < lev
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)
163 local monster_hook = zig().monster_hook
172 local function monster_creator_fn(arg)
173 local atyp = type(arg)
174 if atyp == "string" then
175 local mcreator = zig_monster_fn(arg)
177 local function mspec(x, y, nth)
178 return mcreator(x, y)
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)
186 elseif atyp == "function" then
191 local mons_populations = { }
193 local function mset(...)
194 util.foreach({ ... }, function (spec)
195 table.insert(mons_populations, spec)
199 local function mset_if(condition, ...)
200 mset(unpack(util.map(util.curry(spec_if, condition), { ... })))
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 / " ..
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 }))
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
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"
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
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"
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
303 mset(spec_fn(function ()
304 local d = you.depth() + 5
305 return "place:Tomb:$ w:200 / greater mummy w:" .. d
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
319 mset(spec_fn(function ()
320 local d = 41 - you.depth()
321 return "base draconian w:" .. d .. " / nonbase draconian w:40"
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")
327 local function mons_panlord_gen(x, y, nth)
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)
333 return pan_critter_fn(x, y)
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",
348 function ziggurat_monster_creators()
349 return util.map(monster_creator_fn, mons_populations)
352 local function ziggurat_vet_monster(fmap)
354 fmap.fn = function (x, y, nth, hdmax)
355 if nth >= dgn.MAX_MONSTERS then
359 local mons = fn(x, y, nth)
361 -- Discard zero-exp monsters, and monsters that explode
363 if mons.experience == 0 or mons.hd > hdmax * 1.3 then
377 local function choose_monster_set()
378 return ziggurat_vet_monster(
379 util.random_weighted_from(
381 ziggurat_monster_creators()))
384 -- Function to find travel-safe squares, excluding closed doors.
385 local dgn_passable = dgn.passable_excluding("closed_door")
387 local function ziggurat_create_monsters(p, mfn)
388 local hd_pool = you.depth() * (you.depth() + 8) + 10
392 local function mons_do_place(p)
394 local mons = mfn(p.x, p.y, nth, hd_pool)
398 hd_pool = hd_pool - mons.hd
400 if nth >= dgn.MAX_MONSTERS then
404 -- Can't find any suitable monster for the HD we have left.
410 local function mons_place(point)
413 elseif not dgn.mons_at(point.x, point.y) then
418 dgn.find_adjacent_point(p, mons_place, dgn_passable)
421 local function ziggurat_create_loot_at(c)
422 -- Basically, loot grows linearly with depth.
423 local depth = you.depth()
425 local nloot = depth + crawl.random2(math.floor(nloot * 0.5))
427 local function find_free_space(nspaces)
429 local function add_spaces(p)
433 table.insert(spaces, p)
434 nspaces = nspaces - 1
437 dgn.find_adjacent_point(c, add_spaces, dgn_passable)
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 / " ..
451 local loot_spots = find_free_space(nloot * 4)
453 if #loot_spots == 0 then
458 local function next_loot_spot()
459 curspot = curspot + 1
460 if curspot > #loot_spots then
463 return loot_spots[curspot]
466 local function place_loot(what)
467 local p = next_loot_spot()
468 dgn.create_item(p.x, p.y, what)
472 if crawl.one_chance_in(depth) then
474 place_loot(good_loot)
477 place_loot(super_loot)
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\" })")
490 local has_loot_chamber = false
492 local function ziggurat_create_loot_vault(entry, exit)
493 local inc = (exit - entry):sgn()
495 local connect_point = exit - inc * 3
496 local map = dgn.map_by_tag("ziggurat_loot_chamber")
502 local function place_loot_chamber()
503 local res = dgn.place_map(map, true, true)
505 has_loot_chamber = true
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))
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,
521 return dgn.grid(c.x, c.y) == floor
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()
531 local function connect_loot_chamber()
532 return dgn.with_map_bounds_fn(good_loot_bounds, place_loot_chamber)
535 local res = dgn.with_map_anchors(connect_point.x, connect_point.y,
536 connect_loot_chamber)
540 -- Find the square to drop the loot.
541 local lootx, looty = dgn.find_marker_position_by_prop("ziggurat_loot")
543 if lootx and looty then
544 return dgn.point(lootx, looty)
551 local function ziggurat_locate_loot(entrance, exit, jelly_protect)
552 if jelly_protect then
553 return ziggurat_create_loot_vault(entrance, exit)
559 local function ziggurat_place_pillars(c)
560 local range = crawl.random_range
561 local floor = dgn.fnum("floor")
563 local map, vplace = dgn.resolve_map(dgn.map_by_tag("ziggurat_pillar"))
569 local name = dgn.name(map)
571 local size = dgn.point(dgn.mapsize(map))
573 -- Does the pillar want to be centered?
574 local centered = string.find(dgn.tags(map), " centered ")
576 local function good_place(p)
577 local function good_square(where)
578 return dgn.grid(where.x, where.y) == floor
580 return dgn.rectangle_forall(p, p + size - 1, good_square)
583 local function place_pillar()
585 if good_place(c) then
586 return dgn.place_map(map, true, true, c.x, c.y)
590 local offset = range(-15, -size.x)
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)
598 offsets = util.map(function (o)
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)
607 replace(offsets[1], false, false)
608 replace(offsets[2], false, true)
609 replace(offsets[3], true, false)
610 replace(offsets[4], false, true)
618 if place_pillar() then
624 local function ziggurat_stairs(entry, exit)
625 zigstair(entry.x, entry.y, "stone_arch", "stone_stairs_up_i")
627 if you.depth() < dgn.br_depth(you.branch()) then
628 zigstair(exit.x, exit.y, "stone_stairs_down_i")
631 zigstair(exit.x, exit.y + 1, "exit_ziggurat")
632 zigstair(exit.x, exit.y - 1, "exit_ziggurat")
635 local function ziggurat_furnish(centre, entry, exit)
636 has_loot_chamber = false
637 local monster_generation = choose_monster_set()
639 if type(monster_generation.spec) == "string" then
640 dgn.set_random_mon_list(monster_generation.spec)
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)
648 if not has_loot_chamber then
649 -- Place pillars if we did not create a loot chamber.
650 ziggurat_place_pillars(centre)
653 ziggurat_create_loot_at(lootspot)
655 ziggurat_create_monsters(exit, monster_generation.fn)
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")
662 dgn.colour_map(needs_colour, zig().colour)
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")
671 local area = map_area()
672 area = math.floor(area*3/4)
674 local cx, cy = dgn.GXM / 2, dgn.GYM / 2
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
683 local a = math.floor(math.sqrt(area+4*exc*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")
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")
700 local entry = dgn.point(x1, cy)
701 local exit = dgn.point(x2, cy)
703 if you.depth() % 2 == 0 then
704 entry, exit = exit, entry
707 ziggurat_stairs(entry, exit)
708 ziggurat_furnish(dgn.point(cx, cy), entry, exit)
711 -- builds elliptic ziggurat maps
712 -- given the area, half axes a and b are determined by:
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")
720 local zig_exc = zig().zig_exc
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
727 local floor = dgn.fnum("floor")
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
737 local entry = dgn.point(cx-a+2, cy)
738 local exit = dgn.point(cx+a-2, cy)
740 if you.depth() % 2 == 0 then
741 entry, exit = exit, entry
744 ziggurat_stairs(entry, exit)
745 ziggurat_furnish(dgn.point(cx, cy), entry, exit)
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")
754 local zig_exc = zig().zig_exc
756 local c = dgn.point(dgn.GXM, dgn.GYM) / 2
757 local area = map_area()
759 local a = math.floor(math.sqrt(2 * area / math.sqrt(27))) + 2
760 local b = math.floor(a*math.sqrt(3)/4)
762 local left = dgn.point(math.floor(c.x - (a + math.sqrt(2 * a)) / 2),
764 local right = dgn.point(2 * c.x - left.x, c.y)
766 local floor = dgn.fnum("floor")
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
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
783 local entry = left + dgn.point(1,0)
784 local exit = right - dgn.point(1, 0)
786 if you.depth() % 2 == 0 then
787 entry, exit = exit, entry
790 ziggurat_stairs(entry, exit)
791 ziggurat_furnish(c, entry, exit)
794 ----------------------------------------------------------------------
796 ziggurat_builder_map = {
797 rectangle = ziggurat_rectangle_builder,
798 ellipse = ziggurat_ellipse_builder,
799 hex = ziggurat_hexagon_builder
802 local ziggurat_builders = util.keys(ziggurat_builder_map)
804 function ziggurat_choose_builder()
805 return util.random_from(ziggurat_builders)