-- NetHack themerms.lua $NHDT-Date: 1744445274 2025/04/12 00:07:54 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.40 $ -- Copyright (c) 2020 by Pasi Kallinen -- NetHack may be freely redistributed. See license for details. -- -- themerooms is an array of tables. -- the tables define "name", "frequency", "contents", "mindiff" and "maxdiff". -- * "name" is not shown in-game; it is so that developers can specify a -- certain room to generate by using the THEMERM or THEMERMFILL environment -- variable. While technically optional, it should be provided on all the -- rooms; if it isn't, the room can't be directly specified. -- * "frequency" is optional; if omitted, 1 is assumed. -- * "contents" is a function describing what gets put into the room. -- * "mindiff" and "maxdiff" are optional and independent; if omitted, the -- room is not constrained by level difficulty. -- * "eligible" is optional; if omitted, True is assumed. -- -- themeroom_fills is an array of tables with the exact same structure as -- themerooms. It is used for contents of a room that are independent of its -- shape, so that interestingly-shaped themerooms can be filled with a variety -- of contents. -- * The "contents" functions in themeroom_fills take the room they are -- filling as an argument. -- * Frequency of themeroom_fills is a separate pool from themerooms, and has -- no effect on how likely that any given room will receive a themeroom_fill. -- -- 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 = { { name = "Ice room", contents = 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, }, { name = "Cloud room", contents = 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, }, { name = "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, }, { name = "Spider nest", contents = 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, }, { name = "Trap room", contents = 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, }, { name = "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 }, { name = "Buried treasure", contents = 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, }, { name = "Buried zombies", contents = 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, }, { name = "Massacre", contents = 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, }, { name = "Statuary", contents = 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, }, { name = "Light source", eligible = function(rm) return rm.lit == false; end, contents = function(rm) des.object({ id = "oil lamp", lit = true }); end }, { name = "Temple of the gods", contents = function(rm) des.altar({ align = align[1] }); des.altar({ align = align[2] }); des.altar({ align = align[3] }); end, }, { name = "Ghost of an Adventurer", contents = 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, }, { name = "Storeroom", contents = 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, }, { name = "Teleportation hub", contents = 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 = { { name = "default", frequency = 1000, contents = function() des.room({ type = "ordinary", filled = 1 }); end }, { name = "Fake Delphi", contents = 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, }, { name = "Room in a room", contents = function() des.room({ type = "ordinary", filled = 1, contents = function() des.room({ type = "ordinary", contents = function() des.door({ state="random", wall="all" }); end }); end }); end, }, { name = "Huge room with another room inside", contents = 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, }, { name = "Nesting rooms", contents = 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, }, { name = "Default room with themed fill", frequency = 6, contents = function() des.room({ type = "themed", contents = themeroom_fill }); end }, { name = "Unlit room with themed fill", frequency = 2, contents = function() des.room({ type = "themed", lit = 0, contents = themeroom_fill }); end }, { name = "Room with both normal contents and themed fill", frequency = 2, contents = function() des.room({ type = "themed", filled = 1, contents = themeroom_fill }); end }, { name = 'Pillars', contents = 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, }, { name = 'Mausoleum', contents = 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, }, { name = 'Random dungeon feature in the middle of an odd-sized room', contents = 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, }, { name = 'L-shaped', contents = function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| --------]], contents = function(m) filler_region(1,1); end }); end, }, { name = 'L-shaped, rot 1', contents = function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| --------]], contents = function(m) filler_region(5,1); end }); end, }, { name = 'L-shaped, rot 2', contents = function() des.map({ map = [[ -------- |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(1,1); end }); end, }, { name = 'L-shaped, rot 3', contents = function() des.map({ map = [[ -------- |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(1,1); end }); end, }, { name = 'Blocked center', contents = 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, }, { name = 'Circular, small', contents = function() des.map({ map = [[ xx---xx x--.--x --...-- |.....| --...-- x--.--x xx---xx]], contents = function(m) filler_region(3,3); end }); end, }, { name = 'Circular, medium', contents = function() des.map({ map = [[ xx-----xx x--...--x --.....-- |.......| |.......| |.......| --.....-- x--...--x xx-----xx]], contents = function(m) filler_region(4,4); end }); end, }, { name = 'Circular, big', contents = 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, }, { name = 'T-shaped', contents = function() des.map({ map = [[ xxx-----xxx xxx|...|xxx xxx|...|xxx ----...---- |.........| |.........| |.........| -----------]], contents = function(m) filler_region(5,5); end }); end, }, { name = 'T-shaped, rot 1', contents = function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(2,2); end }); end, }, { name = 'T-shaped, rot 2', contents = function() des.map({ map = [[ ----------- |.........| |.........| |.........| ----...---- xxx|...|xxx xxx|...|xxx xxx-----xxx]], contents = function(m) filler_region(2,2); end }); end, }, { name = 'T-shaped, rot 3', contents = function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(5,5); end }); end, }, { name = 'S-shaped', contents = function() des.map({ map = [[ -----xxx |...|xxx |...|xxx |...---- |......| |......| |......| ----...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(2,2); end }); end, }, { name = 'S-shaped, rot 1', contents = function() des.map({ map = [[ xxx-------- xxx|......| xxx|......| ----......| |......---- |......|xxx |......|xxx --------xxx]], contents = function(m) filler_region(5,5); end }); end, }, { name = 'Z-shaped', contents = function() des.map({ map = [[ xxx----- xxx|...| xxx|...| ----...| |......| |......| |......| |...---- |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(5,5); end }); end, }, { name = 'Z-shaped, rot 1', contents = function() des.map({ map = [[ --------xxx |......|xxx |......|xxx |......---- ----......| xxx|......| xxx|......| xxx--------]], contents = function(m) filler_region(2,2); end }); end, }, { name = 'Cross', contents = 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, }, { name = 'Four-leaf clover', contents = function() des.map({ map = [[ -----x----- |...|x|...| |...---...| |.........| ---.....--- xx|.....|xx ---.....--- |.........| |...---...| |...|x|...| -----x-----]], contents = function(m) filler_region(6,6); end }); end, }, { name = 'Water-surrounded vault', contents = 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 the 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 }); end, }, { name = 'Twin businesses', mindiff = 4, -- arbitrary 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 }, }; -- store these at global scope, they will be reinitialized in -- pre_themerooms_generate debug_rm_idx = nil debug_fill_idx = nil -- Given a point in a themed room, ensure that themed room is stocked with -- regular room contents. -- With 30% chance, also give it a random themed fill. 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 (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 return true end -- given the name of a themed room or fill, return its index in that array function lookup_by_name(name, checkfills) if name == nil then return nil end if checkfills then for i = 1, #themeroom_fills do if themeroom_fills[i].name == name then return i end end else for i = 1, #themerooms do if themerooms[i].name == name then return i end end end return nil end -- called repeatedly until the core decides there are enough rooms function themerooms_generate() if debug_rm_idx ~= nil then -- room may not be suitable for stairs/portals, so create the "default" -- room half of the time -- (if the user specified BOTH a room and a fill, presumably they are -- interested in what happens when that room gets that fill, so don't -- bother generating default-with-fill rooms as happens below) local actualrm = lookup_by_name("default", false); if percent(50) then if is_eligible(themerooms[debug_rm_idx]) then actualrm = debug_rm_idx else pline("Warning: themeroom '"..themerooms[debug_rm_idx].name .."' is ineligible") end end themerooms[actualrm].contents(); return elseif debug_fill_idx ~= nil then -- when a fill is requested but not a room, still create the "default" -- room half of the time, and "default with themed fill" half of the time -- (themeroom_fill will take care of guaranteeing the fill in it) local actualrm = lookup_by_name(percent(50) and "Default room with themed fill" or "default") themerooms[actualrm].contents(); return end local pick = nil; local total_frequency = 0; for i = 1, #themerooms do if (type(themerooms[i]) ~= "table") then nh.impossible('themed room '..i..' is not a table') elseif is_eligible(themerooms[i], nil) then -- Reservoir sampling: select one room from the set of eligible rooms, -- which may change on different levels because of level difficulty. local this_frequency; if (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 if pick == nil then nh.impossible('no eligible themed rooms?') return end themerooms[pick].contents(); end -- called before any rooms are generated function pre_themerooms_generate() local debug_themerm = nh.debug_themerm(false) local debug_fill = nh.debug_themerm(true) local xtrainfo = "" debug_rm_idx = lookup_by_name(debug_themerm, false) debug_fill_idx = lookup_by_name(debug_fill, true) if debug_themerm ~= nil and debug_rm_idx == nil then if lookup_by_name(debug_themerm, true) ~= nil then xtrainfo = "; it is a fill type" end pline("Warning: themeroom '"..debug_themerm .."' not found in themerooms"..xtrainfo, true) end if debug_fill ~= nil and debug_fill_idx == nil then if lookup_by_name(debug_fill, false) ~= nil then xtrainfo = "; it is a room type" end pline("Warning: themeroom fill '"..debug_fill .."' not found in themeroom_fills"..xtrainfo, true) end 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) if debug_fill_idx ~= nil then if is_eligible(themeroom_fills[debug_fill_idx], rm) then themeroom_fills[debug_fill_idx].contents(rm); else -- ideally this would be a debugpline, not a full pline, and offer -- some more context on whether it failed because of difficulty or -- because of eligible function returning false; the warning doesn't -- necessarily mean anything. pline("Warning: fill '"..themeroom_fills[debug_fill_idx].name .."' is not eligible in room that generated it") end return end local pick = nil; local total_frequency = 0; for i = 1, #themeroom_fills do if (type(themeroom_fills[i]) ~= "table") then nh.impossible('themeroom fill '..i..' must be a table') elseif is_eligible(themeroom_fills[i], rm) then -- Reservoir sampling: select one room from the set of eligible rooms, -- which may change on different levels because of level difficulty. local this_frequency; if (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 fill has freq 0 if this_frequency > 0 and nh.rn2(total_frequency) < this_frequency then pick = i; end end end if pick == nil then nh.impossible('no eligible themed room fills?') return end themeroom_fills[pick].contents(rm); 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(); -- change walls to trees des.replace_terrain({ selection = sel, fromterrain="w", toterrain = "T" }); -- update secret doors; attempting to change to AIR will set arboreal flag des.replace_terrain({ selection = sel, fromterrain="S", toterrain = "A" }); 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