Files
nethack/dat/themerms.lua
copperwater 3427c43df2 Guard against no themed rooms or fills being eligible to generate
The themed room code previously assumed that on any given level, at
least one room or fill would resolve as OK to generate there. However,
that's not a great assumption to make, and if it happened to be broken,
the first themed room or fill would arbitrarily be executed, even though
it wasn't eligible. Fix that by setting the initial pick to nil, and
raising an impossible if it's still nil after trying to choose a random
room.
2025-04-11 16:27:34 -07:00

1074 lines
33 KiB
Lua

-- NetHack themerms.lua $NHDT-Date: 1652196294 2022/05/10 15:24:54 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.16 $
-- 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.
--
-- 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 it is 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)
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
pline("Warning: themeroom '"..debug_themerm
.."' not found in themerooms", true)
end
if debug_fill ~= nil and debug_fill_idx == nil then
pline("Warning: themeroom fill '"..debug_fill
.."' not found in themeroom_fills", 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();
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