diff --git a/dat/themerms.lua b/dat/themerms.lua index 454059125..b7e0f1f0a 100644 --- a/dat/themerms.lua +++ b/dat/themerms.lua @@ -2,12 +2,26 @@ -- 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. +-- 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 @@ -24,35 +38,38 @@ -- 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 { + 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); @@ -64,35 +81,39 @@ themeroom_fills = { end end; locs:iterate(func); - end + 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 { + 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(); @@ -107,216 +128,242 @@ themeroom_fills = { 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 { + 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 }, - -- Temple of the gods - function(rm) - des.altar({ align = align[1] }); - des.altar({ align = align[2] }); - des.altar({ align = align[3] }); - 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, + }, - -- 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" }); + { + 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 - 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 } }); + if percent(55) then + des.object({ class = ")", coord = loc, buc = "not-blessed" }); end - end - 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 = { { - -- the "default" room + name = "default", 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, + { + 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 }); @@ -324,6 +371,7 @@ themerooms = { }, { + name = "Unlit room with themed fill", frequency = 2, contents = function() des.room({ type = "themed", lit = 0, contents = themeroom_fill }); @@ -331,71 +379,79 @@ themerooms = { }, { + name = "Room with both normal contents and themed fill", 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, + { + 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, + }, - -- 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, + { + 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, + }, - -- 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, + { + 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, + }, - -- L-shaped - function() - des.map({ map = [[ + { + name = 'L-shaped', + contents = function() + des.map({ map = [[ -----xxx |...|xxx |...|xxx @@ -404,11 +460,13 @@ themerooms = { |......| |......| --------]], contents = function(m) filler_region(1,1); end }); - end, + end, + }, - -- L-shaped, rot 1 - function() - des.map({ map = [[ + { + name = 'L-shaped, rot 1', + contents = function() + des.map({ map = [[ xxx----- xxx|...| xxx|...| @@ -417,11 +475,13 @@ xxx|...| |......| |......| --------]], contents = function(m) filler_region(5,1); end }); - end, + end, + }, - -- L-shaped, rot 2 - function() - des.map({ map = [[ + { + name = 'L-shaped, rot 2', + contents = function() + des.map({ map = [[ -------- |......| |......| @@ -430,11 +490,13 @@ xxx|...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(1,1); end }); - end, + end, + }, - -- L-shaped, rot 3 - function() - des.map({ map = [[ + { + name = 'L-shaped, rot 3', + contents = function() + des.map({ map = [[ -------- |......| |......| @@ -443,11 +505,13 @@ xxx-----]], contents = function(m) filler_region(1,1); end }); |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(1,1); end }); - end, + end, + }, - -- Blocked center - function() - des.map({ map = [[ + { + name = 'Blocked center', + contents = function() + des.map({ map = [[ ----------- |.........| |.........| @@ -459,18 +523,20 @@ xxx-----]], contents = function(m) filler_region(1,1); end }); |.........| |.........| -----------]], 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, + 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 = [[ + { + name = 'Circular, small', + contents = function() + des.map({ map = [[ xx---xx x--.--x --...-- @@ -478,11 +544,13 @@ x--.--x --...-- x--.--x xx---xx]], contents = function(m) filler_region(3,3); end }); - end, + end, + }, - -- Circular, medium - function() - des.map({ map = [[ + { + name = 'Circular, medium', + contents = function() + des.map({ map = [[ xx-----xx x--...--x --.....-- @@ -492,11 +560,13 @@ x--...--x --.....-- x--...--x xx-----xx]], contents = function(m) filler_region(4,4); end }); - end, + end, + }, - -- Circular, big - function() - des.map({ map = [[ + { + name = 'Circular, big', + contents = function() + des.map({ map = [[ xxx-----xxx x---...---x x-.......-x @@ -508,11 +578,13 @@ x-.......-x x-.......-x x---...---x xxx-----xxx]], contents = function(m) filler_region(5,5); end }); - end, + end, + }, - -- T-shaped - function() - des.map({ map = [[ + { + name = 'T-shaped', + contents = function() + des.map({ map = [[ xxx-----xxx xxx|...|xxx xxx|...|xxx @@ -521,11 +593,13 @@ xxx|...|xxx |.........| |.........| -----------]], contents = function(m) filler_region(5,5); end }); - end, + end, + }, - -- T-shaped, rot 1 - function() - des.map({ map = [[ + { + name = 'T-shaped, rot 1', + contents = function() + des.map({ map = [[ -----xxx |...|xxx |...|xxx @@ -537,11 +611,13 @@ xxx|...|xxx |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(2,2); end }); - end, + end, + }, - -- T-shaped, rot 2 - function() - des.map({ map = [[ + { + name = 'T-shaped, rot 2', + contents = function() + des.map({ map = [[ ----------- |.........| |.........| @@ -550,11 +626,13 @@ xxx|...|xxx xxx|...|xxx xxx|...|xxx xxx-----xxx]], contents = function(m) filler_region(2,2); end }); - end, + end, + }, - -- T-shaped, rot 3 - function() - des.map({ map = [[ + { + name = 'T-shaped, rot 3', + contents = function() + des.map({ map = [[ xxx----- xxx|...| xxx|...| @@ -566,11 +644,13 @@ xxx|...| xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(5,5); end }); - end, + end, + }, - -- S-shaped - function() - des.map({ map = [[ + { + name = 'S-shaped', + contents = function() + des.map({ map = [[ -----xxx |...|xxx |...|xxx @@ -582,11 +662,13 @@ xxx-----]], contents = function(m) filler_region(5,5); end }); xxx|...| xxx|...| xxx-----]], contents = function(m) filler_region(2,2); end }); - end, + end, + }, - -- S-shaped, rot 1 - function() - des.map({ map = [[ + { + name = 'S-shaped, rot 1', + contents = function() + des.map({ map = [[ xxx-------- xxx|......| xxx|......| @@ -595,11 +677,13 @@ xxx|......| |......|xxx |......|xxx --------xxx]], contents = function(m) filler_region(5,5); end }); - end, + end, + }, - -- Z-shaped - function() - des.map({ map = [[ + { + name = 'Z-shaped', + contents = function() + des.map({ map = [[ xxx----- xxx|...| xxx|...| @@ -611,11 +695,13 @@ xxx|...| |...|xxx |...|xxx -----xxx]], contents = function(m) filler_region(5,5); end }); - end, + end, + }, - -- Z-shaped, rot 1 - function() - des.map({ map = [[ + { + name = 'Z-shaped, rot 1', + contents = function() + des.map({ map = [[ --------xxx |......|xxx |......|xxx @@ -624,11 +710,13 @@ xxx|...| xxx|......| xxx|......| xxx--------]], contents = function(m) filler_region(2,2); end }); - end, + end, + }, - -- Cross - function() - des.map({ map = [[ + { + name = 'Cross', + contents = function() + des.map({ map = [[ xxx-----xxx xxx|...|xxx xxx|...|xxx @@ -640,11 +728,13 @@ xxx|...|xxx xxx|...|xxx xxx|...|xxx xxx-----xxx]], contents = function(m) filler_region(6,6); end }); - end, + end, + }, - -- Four-leaf clover - function() - des.map({ map = [[ + { + name = 'Four-leaf clover', + contents = function() + des.map({ map = [[ -----x----- |...|x|...| |...---...| @@ -656,59 +746,63 @@ xx|.....|xx |...---...| |...|x|...| -----x-----]], contents = function(m) filler_region(6,6); end }); - end, + end, + }, - -- Water-surrounded vault - function() - des.map({ map = [[ + { + 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 } }; + 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); + 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 + 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, + shuffle(nasty_undead); + des.monster(nasty_undead[1], 2, 2); + des.exclusion({ type = "teleport", region = { 2,2, 3,3 } }); + end }); + end, + }, - -- Twin businesses { - mindiff = 4; -- arbitrary + 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 @@ -761,7 +855,14 @@ 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; @@ -775,31 +876,76 @@ 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 + 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 = 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 + 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 (type(themerooms[i]) == "table" and themerooms[i].frequency ~= nil) then + if (themerooms[i].frequency ~= nil) then this_frequency = themerooms[i].frequency; else this_frequency = 1; @@ -811,17 +957,23 @@ function themerooms_generate() end end end - - local t = type(themerooms[pick]); - if (t == "table") then - themerooms[pick].contents(); - elseif (t == "function") then - themerooms[pick](); - 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 @@ -830,32 +982,41 @@ 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 = 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 + 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 (type(themeroom_fills[i]) == "table" and themeroom_fills[i].frequency ~= nil) then + 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 room has freq 0 + -- 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 - - local t = type(themeroom_fills[pick]); - if (t == "table") then - themeroom_fills[pick].contents(rm); - elseif (t == "function") then - themeroom_fills[pick](rm); - end + themeroom_fills[pick].contents(rm); end -- postprocess callback: create an engraving pointing at a location diff --git a/src/nhlua.c b/src/nhlua.c index 6342f949c..10c67494a 100644 --- a/src/nhlua.c +++ b/src/nhlua.c @@ -65,6 +65,7 @@ staticfn int nhl_rn2(lua_State *); staticfn int nhl_random(lua_State *); staticfn int nhl_level_difficulty(lua_State *); staticfn int nhl_is_genocided(lua_State *); +staticfn int nhl_get_debug_themerm_name(lua_State *); staticfn void init_nhc_data(lua_State *); staticfn int nhl_push_anything(lua_State *, int, void *); staticfn int nhl_meta_u_index(lua_State *); @@ -975,6 +976,30 @@ nhl_is_genocided(lua_State *L) return 1; } +/* local debug_themerm = nh.debug_themerm(isfill) + * if isfill is false, returns value of env variable THEMERM + * if isfill is true, returns value of env variable THEMERMFILL + * return nil if not in wizard mode or the variable isn't set */ +staticfn int +nhl_get_debug_themerm_name(lua_State *L) +{ + int argc = lua_gettop(L); + if (argc == 1) { + char *dbg_themerm = (char *) 0; + boolean is_fill = lua_toboolean(L, 1); + lua_pop(L, 1); + if (wizard) + dbg_themerm = getenv(is_fill ? "THEMERMFILL" : "THEMERM"); + if (!dbg_themerm || strlen(dbg_themerm) == 0) { + lua_pushnil(L); + } else { + lua_pushstring(L, dbg_themerm); + } + } else { + nhl_error(L, "debug_themerm should have 1 boolean arg"); + } + return 1; +} RESTORE_WARNING_UNREACHABLE_CODE @@ -1754,6 +1779,7 @@ static const struct luaL_Reg nhl_functions[] = { { "random", nhl_random }, { "level_difficulty", nhl_level_difficulty }, { "is_genocided", nhl_is_genocided }, + { "debug_themerm", nhl_get_debug_themerm_name }, { "parse_config", nhl_parse_config }, { "get_config", nhl_get_config }, { "get_config_errors", l_get_config_errors },