-- NetHack themerms.lua $NHDT-Date: 1743399789 2025/03/30 21:43:09 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.36 $ -- Copyright (c) 2020 by Pasi Kallinen -- NetHack may be freely redistributed. See license for details. -- -- themerooms is an array of tables and/or functions. -- the tables define "frequency", "contents", "mindiff" and "maxdiff". -- frequency is optional; if omitted, 1 is assumed. -- mindiff and maxdiff are optional and independent; if omitted, the room is -- not constrained by level difficulty. -- a plain function has frequency of 1, and no difficulty constraints. -- des.room({ type = "ordinary", filled = 1 }) -- - ordinary rooms can be converted to shops or any other special rooms. -- - filled = 1 means the room gets random room contents, even if it -- doesn't get converted into a special room. Without filled, -- the room only gets what you define in here. -- - use type = "themed" to force a room that's never converted -- to a special room, such as a shop or a temple. -- -- for each level, the core first calls pre_themerooms_generate(), -- then it calls themerooms_generate() multiple times until it decides -- enough rooms have been generated, and then it calls -- post_themerooms_generate(). When the level has been generated, with -- joining corridors and rooms filled, the core calls post_level_generate(). -- The lua state is persistent through the gameplay, but not across saves, -- so remember to reset any variables. local postprocess = { }; themeroom_fills = { -- Ice room function(rm) local ice = selection.room(); des.terrain(ice, "I"); if (percent(25)) then local mintime = 1000 - (nh.level_difficulty() * 100); local ice_melter = function(x,y) nh.start_timer_at(x,y, "melt-ice", mintime + nh.rn2(1000)); end; ice:iterate(ice_melter); end end, -- Cloud room function(rm) local fog = selection.room(); for i = 1, (fog:numpoints() / 4) do des.monster({ id = "fog cloud", asleep = true }); end des.gas_cloud({ selection = fog }); end, -- Boulder room { mindiff = 4, contents = function(rm) local locs = selection.room():percentage(30); local func = function(x,y) if (percent(50)) then des.object("boulder", x, y); else des.trap("rolling boulder", x, y); end end; locs:iterate(func); end }, -- Spider nest function(rm) local spooders = nh.level_difficulty() > 8; local locs = selection.room():percentage(30); local func = function(x,y) des.trap({ type = "web", x = x, y = y, spider_on_web = spooders and percent(80) }); end locs:iterate(func); end, -- Trap room function(rm) local traps = { "arrow", "dart", "falling rock", "bear", "land mine", "sleep gas", "rust", "anti magic" }; shuffle(traps); local locs = selection.room():percentage(30); local func = function(x,y) des.trap(traps[1], x, y); end locs:iterate(func); end, -- Garden { eligible = function(rm) return rm.lit == true; end, contents = function(rm) local s = selection.room(); local npts = (s:numpoints() / 6); for i = 1, npts do des.monster({ id = "wood nymph", asleep = true }); if (percent(30)) then des.feature("fountain"); end end table.insert(postprocess, { handler = make_garden_walls, data = { sel = selection.room() } }); end }, -- Buried treasure function(rm) des.object({ id = "chest", buried = true, contents = function(otmp) local xobj = otmp:totable(); -- keep track of the last buried treasure if (xobj.NO_OBJ == nil) then table.insert(postprocess, { handler = make_dig_engraving, data = { x = xobj.ox, y = xobj.oy } }); end for i = 1, d(3, 4) do des.object(); end end }); end, -- Buried zombies function(rm) local diff = nh.level_difficulty() -- start with [1..4] for low difficulty local zombifiable = { "kobold", "gnome", "orc", "dwarf" }; if diff > 3 then -- medium difficulty zombifiable[5], zombifiable[6] = "elf", "human"; if diff > 6 then -- high difficulty (relatively speaking) zombifiable[7], zombifiable[8] = "ettin", "giant"; end end for i = 1, (rm.width * rm.height) / 2 do shuffle(zombifiable); local o = des.object({ id = "corpse", montype = zombifiable[1], buried = true }); o:stop_timer("rot-corpse"); o:start_timer("zombify-mon", math.random(990, 1010)); end end, -- Massacre function(rm) local mon = { "apprentice", "warrior", "ninja", "thug", "hunter", "acolyte", "abbot", "page", "attendant", "neanderthal", "chieftain", "student", "wizard", "valkyrie", "tourist", "samurai", "rogue", "ranger", "priestess", "priest", "monk", "knight", "healer", "cavewoman", "caveman", "barbarian", "archeologist" }; local idx = math.random(#mon); for i = 1, d(5,5) do if (percent(10)) then idx = math.random(#mon); end des.object({ id = "corpse", montype = mon[idx] }); end end, -- Statuary function(rm) for i = 1, d(5,5) do des.object({ id = "statue" }); end for i = 1, d(3) do des.trap("statue"); end end, -- Light source { eligible = function(rm) return rm.lit == false; end, contents = function(rm) des.object({ id = "oil lamp", lit = true }); end }, -- Temple of the gods function(rm) des.altar({ align = align[1] }); des.altar({ align = align[2] }); des.altar({ align = align[3] }); end, -- Ghost of an Adventurer function(rm) local loc = selection.room():rndcoord(0); des.monster({ id = "ghost", asleep = true, waiting = true, coord = loc }); if percent(65) then des.object({ id = "dagger", coord = loc, buc = "not-blessed" }); end if percent(55) then des.object({ class = ")", coord = loc, buc = "not-blessed" }); end if percent(45) then des.object({ id = "bow", coord = loc, buc = "not-blessed" }); des.object({ id = "arrow", coord = loc, buc = "not-blessed" }); end if percent(65) then des.object({ class = "[", coord = loc, buc = "not-blessed" }); end if percent(20) then des.object({ class = "=", coord = loc, buc = "not-blessed" }); end if percent(20) then des.object({ class = "?", coord = loc, buc = "not-blessed" }); end end, -- Storeroom function(rm) local locs = selection.room():percentage(30); local func = function(x,y) if (percent(25)) then des.object("chest"); else des.monster({ class = "m", appear_as = "obj:chest" }); end end; locs:iterate(func); end, -- Teleportation hub function(rm) local locs = selection.room():filter_mapchar("."); for i = 1, 2 + nh.rn2(3) do local pos = locs:rndcoord(1); if (pos.x > 0) then pos.x = pos.x + rm.region.x1 - 1; pos.y = pos.y + rm.region.y1; table.insert(postprocess, { handler = make_a_trap, data = { type = "teleport", seen = true, coord = pos, teledest = 1 } }); end end end, }; themerooms = { { -- the "default" room frequency = 1000, contents = function() des.room({ type = "ordinary", filled = 1 }); end }, -- Fake Delphi function() des.room({ type = "ordinary", w = 11,h = 9, filled = 1, contents = function() des.room({ type = "ordinary", x = 4, y = 3, w = 3, h = 3, filled = 1, contents = function() des.door({ state="random", wall="all" }); end }); end }); end, -- Room in a room function() des.room({ type = "ordinary", filled = 1, contents = function() des.room({ type = "ordinary", contents = function() des.door({ state="random", wall="all" }); end }); end }); end, -- Huge room, with another room inside (90%) function() des.room({ type = "ordinary", w = nh.rn2(10) + 11, h = nh.rn2(5) + 8, filled = 1, contents = function() if (percent(90)) then des.room({ type = "ordinary", filled = 1, contents = function() des.door({ state="random", wall="all" }); if (percent(50)) then des.door({ state="random", wall="all" }); end end }); end end }); end, -- Nesting rooms function() des.room({ type = "ordinary", w = 9 + nh.rn2(4), h = 9 + nh.rn2(4), filled = 1, contents = function(rm) local wid = math.random(math.floor(rm.width / 2), rm.width - 2); local hei = math.random(math.floor(rm.height / 2), rm.height - 2); des.room({ type = "ordinary", w = wid,h = hei, filled = 1, contents = function() if (percent(90)) then des.room({ type = "ordinary", filled = 1, contents = function() des.door({ state="random", wall="all" }); if (percent(15)) then des.door({ state="random", wall="all" }); end end }); end des.door({ state="random", wall="all" }); if (percent(15)) then des.door({ state="random", wall="all" }); end end }); end }); end, { frequency = 6, contents = function() des.room({ type = "themed", contents = themeroom_fill }); end }, { frequency = 2, contents = function() des.room({ type = "themed", lit = 0, contents = themeroom_fill }); end }, { frequency = 2, contents = function() des.room({ type = "themed", filled = 1, contents = themeroom_fill }); end }, -- Pillars function() des.room({ type = "themed", w = 10, h = 10, contents = function(rm) local terr = { "-", "-", "-", "-", "L", "P", "T" }; shuffle(terr); for x = 0, (rm.width / 4) - 1 do for y = 0, (rm.height / 4) - 1 do des.terrain({ x = x * 4 + 2, y = y * 4 + 2, typ = terr[1], lit = -2 }); des.terrain({ x = x * 4 + 3, y = y * 4 + 2, typ = terr[1], lit = -2 }); des.terrain({ x = x * 4 + 2, y = y * 4 + 3, typ = terr[1], lit = -2 }); des.terrain({ x = x * 4 + 3, y = y * 4 + 3, typ = terr[1], lit = -2 }); end end end }); end, -- Mausoleum function() des.room({ type = "themed", w = 5 + nh.rn2(3)*2, h = 5 + nh.rn2(3)*2, contents = function(rm) des.room({ type = "themed", x = (rm.width - 1) / 2, y = (rm.height - 1) / 2, w = 1, h = 1, joined = false, contents = function() if (percent(50)) then local mons = { "M", "V", "L", "Z" }; shuffle(mons); des.monster({ class = mons[1], x=0, y=0, waiting = 1 }); else des.object({ id = "corpse", montype = "@", coord = {0,0} }); end if (percent(20)) then des.door({ state="secret", wall="all" }); end end }); end }); end, -- Random dungeon feature in the middle of an odd-sized room function() local wid = 3 + (nh.rn2(3) * 2); local hei = 3 + (nh.rn2(3) * 2); des.room({ type = "ordinary", filled = 1, w = wid, h = hei, contents = function(rm) local feature = { "C", "L", "I", "P", "T" }; shuffle(feature); des.terrain((rm.width - 1) / 2, (rm.height - 1) / 2, feature[1]); end }); end, -- L-shaped function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| --------]], contents = function(m) filler_region(1,1); end }); end, -- L-shaped, rot 1 function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| --------]], contents = function(m) filler_region(5,1); end }); end, -- L-shaped, rot 2 function() des.map({ map = [[ -------- |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(1,1); end }); end, -- L-shaped, rot 3 function() des.map({ map = [[ -------- |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(1,1); end }); end, -- Blocked center function() des.map({ map = [[ ----------- |.........| |.........| |.........| |...LLL...| |...LLL...| |...LLL...| |.........| |.........| |.........| -----------]], contents = function(m) if (percent(30)) then local terr = { "-", "P" }; shuffle(terr); des.replace_terrain({ region = {1,1, 9,9}, fromterrain = "L", toterrain = terr[1] }); end filler_region(1,1); end }); end, -- Circular, small function() des.map({ map = [[ xx---xx x--.--x --...-- |.....| --...-- x--.--x xx---xx]], contents = function(m) filler_region(3,3); end }); end, -- Circular, medium function() des.map({ map = [[ xx-----xx x--...--x --.....-- |.......| |.......| |.......| --.....-- x--...--x xx-----xx]], contents = function(m) filler_region(4,4); end }); end, -- Circular, big function() des.map({ map = [[ xxx-----xxx x---...---x x-.......-x --.......-- |.........| |.........| |.........| --.......-- x-.......-x x---...---x xxx-----xxx]], contents = function(m) filler_region(5,5); end }); end, -- T-shaped function() des.map({ map = [[ xxx-----xxx xxx|...|xxx xxx|...|xxx ----...---- |.........| |.........| |.........| -----------]], contents = function(m) filler_region(5,5); end }); end, -- T-shaped, rot 1 function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(2,2); end }); end, -- T-shaped, rot 2 function() des.map({ map = [[ ----------- |.........| |.........| |.........| ----...---- xxx|...|xxx xxx|...|xxx xxx-----xxx]], contents = function(m) filler_region(2,2); end }); end, -- T-shaped, rot 3 function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(5,5); end }); end, -- S-shaped function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(2,2); end }); end, -- S-shaped, rot 1 function() des.map({ map = [[ xxx-------- xxx|......| xxx|......| ----......| |......---- |......|xxx |......|xxx --------xxx]], contents = function(m) filler_region(5,5); end }); end, -- Z-shaped function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(5,5); end }); end, -- Z-shaped, rot 1 function() des.map({ map = [[ --------xxx |......|xxx |......|xxx |......---- ----......| xxx|......| xxx|......| xxx--------]], contents = function(m) filler_region(2,2); end }); end, -- Cross function() des.map({ map = [[ xxx-----xxx xxx|...|xxx xxx|...|xxx ----...---- |.........| |.........| |.........| ----...---- xxx|...|xxx xxx|...|xxx xxx-----xxx]], contents = function(m) filler_region(6,6); end }); end, -- Four-leaf clover function() des.map({ map = [[ -----x----- |...|x|...| |...---...| |.........| ---.....--- xx|.....|xx ---.....--- |.........| |...---...| |...|x|...| -----x-----]], contents = function(m) filler_region(6,6); end }); end, -- Water-surrounded vault function() des.map({ map = [[ }}}}}} }----} }|..|} }|..|} }----} }}}}}}]], contents = function(m) des.region({ region = {3, 3, 3, 3}, type = "themed", irregular = true, filled = 0, joined = false }); local nasty_undead = { "giant zombie", "ettin zombie", "vampire lord" }; local chest_spots = { { 2, 2 }, { 3, 2 }, { 2, 3 }, { 3, 3 } }; shuffle(chest_spots) -- Guarantee an escape item inside one of the chests, to prevent the -- hero falling in from above and becoming permanently stuck -- [cf. generate_way_out_method(sp_lev.c)]. -- If the escape item is made of glass or crystal, make sure that -- the chest isn't locked so that kicking it to gain access to its -- contents won't be necessary; otherwise retain lock state from -- random creation. "pick-axe", "dwarvish mattock" could be included -- in list of escape items but don't normally generate in containers. local escape_items = { "scroll of teleportation", "ring of teleportation", "wand of teleportation", "wand of digging" }; local itm = obj.new( escape_items[math.random(#escape_items)] ); local itmcls = itm:class() local box if itmcls[ "material" ] == "glass" then -- explicitly force chest to be unlocked box = des.object({ id = "chest", coord = chest_spots[1], olocked = "no" }); else -- accept random locked/unlocked state box = des.object({ id = "chest", coord = chest_spots[1] }); end; box:addcontent(itm); for i = 2, #chest_spots do des.object({ id = "chest", coord = chest_spots[i] }); end shuffle(nasty_undead); des.monster(nasty_undead[1], 2, 2); des.exclusion({ type = "teleport", region = { 2,2, 3,3 } }); end }); -- des.map end, -- Twin businesses { mindiff = 4; -- arbitrary; needs to be greater than 1 since no shops on 1 contents = function() -- Due to the way room connections work in mklev.c, we must guarantee -- that the "aisle" between the shops touches all four walls of the -- larger room. Thus it has an extra width and height. des.room({ type="themed", w=9, h=5, contents = function() -- There are eight possible placements of the two shops, four of -- which have the vertical aisle in the center. southeast = function() return percent(50) and "south" or "east" end northeast = function() return percent(50) and "north" or "east" end northwest = function() return percent(50) and "north" or "west" end southwest = function() return percent(50) and "south" or "west" end placements = { { lx = 1, ly = 1, rx = 4, ry = 1, lwall = "south", rwall = southeast() }, { lx = 1, ly = 2, rx = 4, ry = 2, lwall = "north", rwall = northeast() }, { lx = 1, ly = 1, rx = 5, ry = 1, lwall = southeast(), rwall = southwest() }, { lx = 1, ly = 1, rx = 5, ry = 2, lwall = southeast(), rwall = northwest() }, { lx = 1, ly = 2, rx = 5, ry = 1, lwall = northeast(), rwall = southwest() }, { lx = 1, ly = 2, rx = 5, ry = 2, lwall = northeast(), rwall = northwest() }, { lx = 2, ly = 1, rx = 5, ry = 1, lwall = southwest(), rwall = "south" }, { lx = 2, ly = 2, rx = 5, ry = 2, lwall = northwest(), rwall = "north" } } ltype, rtype = "weapon shop", "armor shop" if percent(50) then ltype, rtype = rtype, ltype end shopdoorstate = function() if percent(1) then return "locked" elseif percent(50) then return "closed" else return "open" end end p = placements[ d(#placements) ] des.room({ type = ltype, x = p["lx"], y = p["ly"], w = 3, h = 3, filled = 1, joined = false, contents = function() des.door({ state = shopdoorstate(), wall = p["lwall"] }) end }); des.room({ type = rtype, x = p["rx"], y = p["ry"], w = 3, h = 3, filled = 1, joined = false, contents = function() des.door({ state = shopdoorstate(), wall = p["rwall"] }) end }); end }); end }, }; function filler_region(x, y) local rmtyp = "ordinary"; local func = nil; if (percent(30)) then rmtyp = "themed"; func = themeroom_fill; end des.region({ region = {x,y,x,y}, type = rmtyp, irregular = true, filled = 1, contents = func }); end function is_eligible(room, mkrm) local t = type(room); local diff = nh.level_difficulty(); if (t == "table") then if (room.mindiff ~= nil and diff < room.mindiff) then return false elseif (room.maxdiff ~= nil and diff > room.maxdiff) then return false end if (mkrm ~= nil and room.eligible ~= nil) then return room.eligible(mkrm); end elseif (t == "function") then -- functions currently have no constraints end return true end -- called repeatedly until the core decides there are enough rooms function themerooms_generate() local pick = 1; local total_frequency = 0; for i = 1, #themerooms do -- Reservoir sampling: select one room from the set of eligible rooms, -- which may change on different levels because of level difficulty. if is_eligible(themerooms[i], nil) then local this_frequency; if (type(themerooms[i]) == "table" and themerooms[i].frequency ~= nil) then this_frequency = themerooms[i].frequency; else this_frequency = 1; end total_frequency = total_frequency + this_frequency; -- avoid rn2(0) if a room has freq 0 if this_frequency > 0 and nh.rn2(total_frequency) < this_frequency then pick = i; end end end local t = type(themerooms[pick]); if (t == "table") then themerooms[pick].contents(); elseif (t == "function") then themerooms[pick](); end end -- called before any rooms are generated function pre_themerooms_generate() end -- called after all rooms have been generated -- but before creating connecting corridors/doors, or filling rooms function post_themerooms_generate() end function themeroom_fill(rm) local pick = 1; local total_frequency = 0; for i = 1, #themeroom_fills do -- Reservoir sampling: select one room from the set of eligible rooms, -- which may change on different levels because of level difficulty. if is_eligible(themeroom_fills[i], rm) then local this_frequency; if (type(themeroom_fills[i]) == "table" and themeroom_fills[i].frequency ~= nil) then this_frequency = themeroom_fills[i].frequency; else this_frequency = 1; end total_frequency = total_frequency + this_frequency; -- avoid rn2(0) if a room has freq 0 if this_frequency > 0 and nh.rn2(total_frequency) < this_frequency then pick = i; end end end local t = type(themeroom_fills[pick]); if (t == "table") then themeroom_fills[pick].contents(rm); elseif (t == "function") then themeroom_fills[pick](rm); end end -- postprocess callback: create an engraving pointing at a location function make_dig_engraving(data) local floors = selection.negate():filter_mapchar("."); local pos = floors:rndcoord(0); local tx = data.x - pos.x - 1; local ty = data.y - pos.y; local dig = ""; if (tx == 0 and ty == 0) then dig = " here"; else if (tx < 0 or tx > 0) then dig = string.format(" %i %s", math.abs(tx), (tx > 0) and "east" or "west"); end if (ty < 0 or ty > 0) then dig = dig .. string.format(" %i %s", math.abs(ty), (ty > 0) and "south" or "north"); end end des.engraving({ coord = pos, type = "burn", text = "Dig" .. dig }); end -- postprocess callback: turn room walls into trees function make_garden_walls(data) local sel = data.sel:grow(); des.replace_terrain({ selection = sel, fromterrain="w", toterrain = "T" }); end -- postprocess callback: make a trap function make_a_trap(data) if (data.teledest == 1 and data.type == "teleport") then local locs = selection.negate():filter_mapchar("."); repeat data.teledest = locs:rndcoord(1); until (data.teledest.x ~= data.coord.x and data.teledest.y ~= data.coord.y); end des.trap(data); end -- called once after the whole level has been generated function post_level_generate() for i, v in ipairs(postprocess) do v.handler(v.data); end postprocess = { }; end