Modding:Tutorial/The Infinite Arena

From DoomRL Wiki

Revision as of 20:34, 6 October 2011 by Game Hunter (Talk | contribs)

Jump to: navigation, search

(NOTE: if you are unfamiliar with the general structure of the Hell's Arena module, it is suggested that you read the tutorial before proceeding, as many of the topics found there also are here, and will only be touched upon in this tutorial.)

Hell's Arena is a fun little level, as it can be fairly difficult to complete without some grasp on DoomRL's mechanics, but pillar and monster placement aside, it is still a fairly static map. The following tutorial is a generalized version of Hell's Arena, coined the Infinite Arena, which has been reconstructed based on yaflhdztioxo's design from v0.9.9.1. A fair number of seemingly-superfluous tools have also been created as a means to provide easy customization possibilities for those who want to quickly change the settings of the Arena. If nothing else, you will discover a variety of helper tools in this tutorial that may prove to be useful in your own modules.

Contents

Special Concept: Custom Tables and Functions

The basics of tables have already been explained in the Game Objects tutorial: it was also explained there that building your own tables is generally unnecessary. There are, however, plenty of reasons to make tables for your own purposes:

  • You want the game to randomly choose from a group of variables
  • You want to perform calculations or subroutines on many objects simultaneously
  • You are creating an object map that other functions may require (e.g., translation table for .run() )
  • You want to store a number of variables in an organized manner in order to access them easily

There are two kinds of tables. The first kind are what are more commonly referred to as tables, as they contain keys and values in each of their fields:

local some_table = {
    key1 = "value1",
    key2 = "value2",
    ....
}

In fact, these tables can contain more tables, like so:

local big_table = {
    small_table1 = {
        key1 = "value1",
        key2 = "value2",
    },
    small_table2 = {
        key1 = "value3",
        key3 = "value4",
    },
}

Note that these sub-tables do not have to carry the same information, although you may want them to (depending on the table's purpose). In doing so, you can create very large and intricate tables to suit your needs.

The second kind of table is commonly known in the modding community as an array, and is essentially a table that does not explicitly write keys in each field. The following examples show an array, followed by a table that imitates the functionality of an array:

local some_array = {"value1","value2","value3"}
 
local some_imit_table = {
    [1] = "value1",
    [2] = "value2",
    [3] = "value3",
}
 
local big_array = {
    {"value1","value2","value3"},
    {"value4","value5","value6"},
}
 
local big_imit_table = {
    [1] = {
        [1] = "value1",
        [2] = "value2",
        [3] = "value3",
    },
    [2] = {
        [1] = "value4",
        [2] = "value5",
        [3] = "value6",
    }, 
}

As you can see, the array format is a lot more compact, but it also forces very specific keys on each field (namely numerical ones). Naturally, tables and arrays can be combined seamlessly, such that you can have a table of arrays or an array or tables (as well as more complicated setups).

When you want to call various parts within a table or array, you can use the following syntax (we will base these on big_array and big_table):

big_table.small_table1        --returns the contents of small_table1
big_table[small_table1]       --identical to above
 
big_table.small_table1.key1   --returns "value1"
big_table.small_table1[key1]  --identical to above
big_table[small_table1].key1  --identical to above
big_table[small_table1][key1] --identical to above
 
big_array.1     --returns the contents of the first array in big_array
big_array[1]    --identical to above
 
big_array.2.2   --returns "value5"
big_array.2[2]  --identical to above
big_array[2].2  --identical to above
big_array[2][2] --identical to above

As a matter of convention, we will write table-like extensions using the dot syntax and array-like extensions using the bracket syntax. (This is typically how other languages write their arrays and tables, and it is often a good idea to organize your code in such a manner so that it is easier to read when looking back at it.)

Functions, like tables, are mostly known based on using parts of pre-defined object classes: in the case of functions, we should already be fairly familiar with their capabilities when calling engine hooks and API methods. The creation of a function is very simple, nearly identical to engine hooks.

local function sum_diff(arg1,arg2)
    val1 = arg1+arg2
    val2 = arg1-arg2
    return(val1,val2)
end
 
local add_diff = function(arg1,arg2) --this line is identical to the first line of the above function
    ....
end
  • First you must give the function a name: the above example is called sum_diff().
  • Next you must supply input arguments, if any. These are variables that can be used within the function, which is otherwise self-contained. That is to say, a function can't use any variables from anywhere else in your lua script unless they are given through the input arguments. The above example takes in two arguments, generically named arg1 and arg2.
  • From here we add the script that is to be run when the function is called (this is usually what you concern yourself with when working with the engine hooks). In the above example, the sum of arg1 and arg2 is assigned to val1, and their difference is assigned to val2.
  • Finally, we consider what the function should output. This can be accomplished in two ways when modding:
    • You can simply provide outputs to the function using the return() core method. This immediately stops the function and sends outputs to the line that called the function. In the above example, val1 and val2 are returned. (Note that, when called, the script should contain some variables so that the return values are properly assigned.)
    • You can manipulate objects. Since changing objects (such as their prototype) modify things on the engine side of the game, this can be done without having any particular outputs to the function. Even though the function is self-contained within your script, it can influence things not belonging to the script.

If you want to call a function, use its name with the appropriate input and outputs. For the above example, you could call the function in the following way:

local num1 = 5
local num2 = 4
 
local sum,diff = sum_diff(num1,num2)

The sum variable will have a value of 9, and the diff variable will have a value of 1.

In the Infinite Arena module, a large portion of the code is dedicated to constructing easy-to-read and well-organized tables, as well as functions that make use of them. The tables and functions will eventually be called in the engine hooks, which leaves us with a relatively small amount of run-time code to sift through. Ideally, modules should have a format in which much of the script is written not as a part of the core module, but as something that the core module will refer to.

Initialization Code

Variables

mobGenStats

--customize minLev/maxLev/weight of each enemy (defaults given)
--for names of beings, check Object IDs in the modding documentation
--be aware that JC auto-ends the game unless modified directly
local mobGenStats = {
    { being = "former",         minLev = 0,   maxLev = 12,  weight = 10 },
    { being = "sergeant",       minLev = 2,   maxLev = 15,  weight = 10 },
    { being = "captain",        minLev = 8,   maxLev = 15,  weight = 10 },
    ....
}

mobGenStats is an array of tables: each table contains four fields relating to the being prototype. In each case, a being's identifier being is given, followed by a minLev, maxLev, and weight (all generation parameters). The purpose of mobGenStats is to switch particular values for others in the being prototypes that already exist. Normally this can be done for a particular enemy with the following line:

beings.former.weight = 0

This would change the weight key in the former table of the beings array (which holds all being prototypes) to zero, thereby removing the possibility of former humans appearing through standard generation procedures. However, if a modder wants to change many parameters at once, they can use what is done in the Infinite Arena. First is the creation of the above table, and then you use the table in a for loop as follows:

--changes the generation stats of enemies based on mobGenStats
for _,v in ipairs(mobGenStats) do
    beings[v.being].minLev = v.minLev
    beings[v.being].maxLev = v.maxLev
    beings[v.being].weight = v.weight
end

The conditional statement 'for a,b in ipairs(array)' breaks down in the following way:

  • a is the iterator of the for loop itself (whenever the code repeats, a increases by one)
    • As we have no need for a basic iterator, we leave it blank using an underscore.
  • b is the array iterator (directly calls the particular array, similarly increments)
  • in ipairs() tells the for loop to iterate through values in an array
  • array is the array in question

In this particular case, the loop iterates through each array and sets the being's minLev, maxLev, and weight (as found in the being prototype) to the values as determined by mobGenStats. v.being is the same as a being's identifier from mobGenStats, which is how the loop iterates through all being prototypes. In this way all of the beings in the prototype array can be changed quickly without a bunch of lines specifying each key and value.

--enemy scaling for each wave: note that game difficulty will modify scaling independently
local mob_scale_factor = 0.5

Finally, we have a variable that indicates the initial scaling of enemies. For the Infinite Arena, using the base game's scaling is probably too difficult for what a player at a given difficulty would expect, so it is cut in half by default.

itemGenStats

--customize level/weight of items (defaults given)
--for names of items, check Object IDs in the modding documentation
local itemGenStats = {
    --Commons
 
    --weapons
    { item = "knife",        level = 1,  weight = 640 },
    { item = "pistol",       level = 1,  weight = 70  },
    { item = "shotgun",      level = 2,  weight = 180 },
    ....
    --ammo/ammo packs
    ....
    --armor/boots
    ....
    --consumables
    ....
    --powerups
    ....    
    --Exotics
    ....    
    --Uniques
    ....    
}

itemGenStats is just like mobGenStats, except that it is an array of tables regarding generation parameters for items. Note that mobGenStats and itemGenStats in the source set all of the monsters and items to their default settings, so there will be no difference between the generation in the base game and in the Infinite Arena unless the modder chooses to customize it personally. (mobGenStats and itemGenStats are also shortened here: see Review for the full arrays.)

--changes the generation stats of items based on itemGenStats
for _,v in ipairs(itemGenStats) do
    items[v.item].level  = v.level
    items[v.item].weight = v.weight
end

As with mobGenStats, a for loop is used to run through all of the variables in the itemGenStats tables and change the prototypes to anything that the modder wishes to customize.

--item scaling for each wave
local item_scale_factor = 0.5

The item scaling is also dropped to scale properly with the drop in monster scaling. (Feel free to change these around for your own amusement, of course.)

anncrMsg and crowdMsg

--add your own announcer lines here
local anncrMsg = {
    --displays whenever player completes wave
    success = {
        {
        "The voice booms, \"Congratulations mortal!",
        "The voice booms, \"Impressive mortal!",
        "The voice booms, \"Most impressive.",
        "The voice booms, \"You are a formidable warrior!",
        },
        {
        ....
        },
        {
        ....
        }
    },
    --displays whenever player decides to continue to another wave
    choice = {
    ....
    }
}

anncrMsg is the opposite of the generation variables: it is a table of arrays. There are two arrays within anncrMsg labeled success and choice, each of which carry their own groups of variables. anncrMsg itself is called whenever the announcer displays a message (after the third wave), using either success (whenever the player completes the wave) or choose (whenever the player chooses to continue to the next wave). Since each group of strings should be handled separately, they are grouped separately, hence the need for a table of arrays.

In addition, anncrMsg.success and anncrMsg.choose are an array of arrays themselves. The reason for this will be explained in build_announcerMsg().

--add your own crowd lines here
--displays whenever enemy is killed
local crowdMsg = {
    "The crowd goes wild! \"BLOOD! BLOOD!\"",
    "The crowd cheers! \"Blood! Blood!\"",
    "The crowd cheers! \"Kill! Kill!\"",
    "The crowd hisses. \"We came for a REAL fight!\"",
    "The crowd boos. \"No skill, no kill!\"",
}

By contrast, crowdMsg is a simple array of strings, called whenever a crowd message is necessary. Since the crowd only needs to be called in one case, only one group of strings is initialized, and so a table of arrays is no longer required.

Functions

build_gear()

--changes player equipment
--add "nil" for any slot that should be empty
--always refreshes all slots: do not use to change only one equipment slot
local function build_gear(weap,prep,body,foot)
    player.eq:clear()
    if weap then player.eq.weapon   = item.new(weap) end --main weapon
    if prep then player.eq.prepared = item.new(prep) end --prepared weapon	
    if body then player.eq.armor    = item.new(body) end --body armor
    if foot then player.eq.boots    = item.new(foot) end --boots
end

build_gear() is an example of a "helper function", as it runs a procedure that could easily be reproduced in the run-time code but simplifies the input whenever the code needs to be modified. In this case, the function clears all of the player's equipment and potentially adds a new item in each slot. There are four inputs, each corresponding to a particular slot: if the value in an input argument is not nil, it then adds a new item to that player's equipment slot.

In the run-time code, the player need only change the input variables rather than changing several lines. This may not appear very necessary for build_gear() but the concept of a helper function can make modifications to code much easier.

build_pack()

--changes player inventory
--always refreshes all slots: do not use to change only one equipment slot
local function build_pack(itemPack)
    player.inv:clear()
    for _,part in ipairs(itemPack) do
        if items[part.name].type == ITEMTYPE_AMMO then
            --ammo is handled separately, loops for however many stacks are needed
            for n=1,math.ceil(part.amt/items[part.name].ammomax) do
                local pack = item.new(part.name)
                if part.amt < pack.ammomax then
                    --remainder (or single) stack
                    pack.ammo = part.amt
                else
                    --filled (and/or multiple) stacks
                    pack.ammo = pack.ammomax
                    part.amt = part.amt - pack.ammomax
                end
                player.inv:add(pack)
            end
        else
            --loops for however many items are needed
            for n=1,part.amt do
                player.inv:add(part.name)
            end
        end
    end
end

build_pack() is another helper function, this time clearing the inventory of the player and adding in new items there. It uses the "in ipairs()" iteration method to check all variables in an array in the following way:

  • if the item's type is ITEMTYPE_AMMO:
    • a new for loop is run, iterating from 1 to ammonum/ammomax for the specific ammo (ammonum currently being how much that the player's inventory receives)
    • first, the new ammo stack is created
    • if ammonum is less than the ammo's ammomax:
      • set the ammo stack equal to ammonum
    • otherwise:
      • set the ammo stack equal to ammomax
      • subtract ammonum by ammomax
    • finally, add the ammo stack to the player's inventory (and this process repeats as necessary)
  • if the item's type is not ITEMTYPE_AMMO:
    • a new foor loop is run, iterating as many times as items should be added for the particular item in question
    • the item(s) are then added to the player's inventory, one by one

The input requires an array of tables, although the format is rather simple. Here is an example of an input for build_pack():

local itemPack = {
    {name = "chaingun", amt = 1  }
    {name = "ammo",     amt = 360}
    {name = "smed",     amt = 5  }
}

Each table in the array requires only two fields: name, which corresponds to the item's identifier, and amt, which specifies how many of that item should be added. In the case of ammunition, the amount of ammo should be specified instead, as it is handled uniquely in order to produce an ideal number of stacks. The above example would add one chaingun, three 9mm ammo (x100) stacks, one 9mm ammo (x60) stack, and five small med-packs to the player's inventory. Keep in mind that the player can only hold 22 items, and any items added after 22 items have been added to the inventory will be disregarded.

build_announcerMsg()

local function build_announcerMsg(MSG)
    for _,msgNum in ipairs(MSG) do
        ui.msg(table.random_pick(msgNum))
    end
end

The build_announcerMsg() function iterates through an array and selects a random index from a table within the array. The purpose of this simple function is to loop through a series of arrayed strings from anncrMsg and randomly pick strings from those arrays. build_announcerMsg(anncrMsg.success) sets up three random messages from each array in its array, and build_announcerMsg(anncrMsg.choose) sets up two random messages from each array in its array. (Setting up a lot of tables within tables can be confusing, but it becomes relatively simple as a means to organize variables together when compared to keeping track of each variable separately.)

get_corpses() and clear_corpses()

--initializes table of cells that only contains corpses
local corpseCells = {}
 
local function get_corpses()
    for i = 1, #cells do
        if cells[i] and cells[i].flags then
            if cells[i].flag_set[CF_CORPSE] == true then
                table.insert(corpseCells, i) --i == sID's numeric value
            end
        end
    end
end

get_corpses() is a quick function that creates a table, corpseCells, and loops through all cells in the cell prototype array, selecting only those that contain the CF_CORPSE flag (indicating that the cell acts like a corpse) and adding it to corpseCells. This method is often the most effective way to create a selective table of objects.

--removes corpses and blood-like cells
local function clear_corpses()
    --fade away all blood
    Generator.transmute("blood", "floor")
    Generator.transmute("bloodpool", "blood")
    --changes corpses to blood
    for i = 1, #corpseCells do
        Generator.transmute(corpseCells[i], "bloodpool")
    end
end

'clear_corpses() puts our table of corpses to use by transmuting any cells contained in the table into the "bloodpool" cell. Additionally, all blood pools are changed to "blood" and all blood is changed to "floor". This is added to the Infinite Arena so that a ridiculous number of corpses isn't around when Arch-vile enemies begin to show themselves.

Engine Hooks

.run()

--Creation of Infinite Arena
function inf_arena.run()
    Level.name = "Infinite Arena"
    Level.name_number = 0
    Level.fill("prwall")
 
    local translation = {
    ["."] = "floor",
    ["#"] = "prwall",
    ["X"] = "pwall",
    [","] = "blood",
    [">"] = "stairs",
    }
 
    local map = [[
#######################.............................########################
###########.....................................................############
#####..................................................................#####
##........................................................................##
#..........................................................................#
....,.......................................................................
.................................,...,......................................
.,.....................................,....................................
..,>.,..........................,...........................................
.,..,.................................,.,...................................
................................,..,...,....................................
............................................................................
............................................................................
#..........................................................................#
##........................................................................##
#####..................................................................#####
###########.....................................................############
#######################.............................########################
]]
    --change up the column types
    local column = {
    [[
,..,.,
,XXXX.
.X##X,
.XXXX.
,..,.,
    ]],
    [[
,..,.,
,X##X.
.####,
.X##X.
,..,.,
    ]],
    [[
,..,.,
,####.
.####,
.####.
,..,.,
    ]]
    }
 
    Level.place_tile(translation,map,2,2)
    for i=1,11 + math.random(4) do
        Level.scatter_put( area.new(5,3,68,15), translation, table.random_pick(column), "floor", 1)
    end
    Level.scatter(area.FULL_SHRINKED, "floor", "blood", 100)
    Level.player(38, 10)
end

The run() hook is very similar to that of the Hell's Arena one. In fact, other than the name change, the only real difference is that the column string has now been expanded to an array of strings, corresponding to an array of pillars. In order to randomly select pillars, Level.scatter_put() must be iterated a number of times, adding only one pillar each time: then, by adding the table.random_pick() method to the tiles-to-be-added argument, it randomly selects one of the pillars in the column array. The result is that the columns randomly have varying degrees of destructibility.

.OnEnter()

function inf_arena.OnEnter()
    --inventory table used for build_pack()
    local itemPack = {
        {name = "smed",  amt = 2},
        {name = "shell", amt = 50},
    }
    --set up starting eq/inv
    build_gear("shotgun","pistol",nil,nil)
    build_pack(itemPack)
 
    --Print announcer messages
    ui.msg("A devilish voice announces: \"Welcome to Hell's Arena, mortal! " ..
           "\"You are either very brave or very foolish. Either way I like it! " ..
           "\"And so do the crowds!\" Suddenly you hear screams everywhere! " ..
	   "\"Blood! Blood! BLOOD!\" \"Kill all enemies and I shall reward thee!\"")
 
    --spawn first wave
    Level.flood_monsters(Generator.being_weight()*mob_scale_factor)
    Level.result(1)
end

OnEnter() makes significant use of the helper functions, using build_gear() and build_pack() to initialize the player's starting equipment and inventory. Besides the itemPack array, this only takes two lines of code, producing a much cleaner and more organized routine than in the original Hell's Arena.

In addition, rather than directly spawning enemies using the Level.summon() method, we imitate the base game's generation code by combining Generator.being_weight(), which outputs the danger level for a given map level, and Level.flood_monsters(), which randomly drops enemies on the level for a given danger level. Additionally, the mob_scale_factor variable is used to change the danger level, thereby changing the scaling of the first wave.

.OnKill()

function inf_arena.OnKill(being)
    --random message from crowdMsg array
    ui.msg(table.random_pick(crowdMsg))
end

OnKill() does exactly the same thing as the one from Hell's Arena, except that the crowd messages have been tabulated and table.random_pick() is used to choose from one of said messages. Technically, build_announcerMsg() would also work here, but since both would only require one line of code, it may as well be the simplest one to understand.

.OnKillAll()

function inf_arena.OnKillAll()
    --print more announcer stuff
    if Level.result() == 1 then
        ui.msg("The voice booms, \"Not bad mortal! For a weakling that you are, " ..
               "you show some determination.\" You hear screams everywhere! " ..
               "\"More Blood! More BLOOD!\" The voice continues, \"I can now " ..
               "let you go free, or you may try to complete the challenge!\"")
    elseif Level.result() == 2 then
        ui.msg("The voice booms, \"Impressive mortal! Your determination to " ..
               "survive makes me excited!\" You hear screams everywhere! " ..
               "\"More Blood! More BLOOD!\" \"I can let you go now, and give you " ..
               "a small reward, or you can choose to fight an additional challenge!\"")
    else
        --random message from anncrMsg.success array
        build_announcerMsg(anncrMsg.success)
    end
    ....

OnKillAll() has received some simplifications similar to OnEnter. To start, after the first two waves, rather than dealing with an large number of random messages here, all of it is called from the previous anncrMsg table and the build_announcerMsg() function, which randomly picks a string from an array strings, then iterates through the entire array of arrays. In the displayed run-time code, this amounts of a single line (although it is actually running quite a few lines).

    ....
    local choice = ui.msg_confirm("Round " .. Level.result() + 1 .. " awaits. " ..
                                  "Do you want to continue the fight?")
    if choice == true then --continuing
        --random message from anncrMsg.choice array
        build_announcerMsg(anncrMsg.choice)
        --set up the danger level for the next wave
        Level.result(Level.result() + 1);
        Level.danger_level = Level.result();
        --spawn items for next wave
        Level.flood_items(Generator.item_amount()*item_scale_factor)
        --spawn enemies for next wave
        Level.flood_monsters(Generator.being_weight()*mob_scale_factor)
    ....

Upon choosing to continue, we use build_announcerMsg() for more messages (again, only a single line of code). Then, we increment the level by one and increase the danger_level using the amount from Level.result(). (Level.danger_level is actually a property of the level object.) Finally, we use the increased danger level to add items and monsters. Level.flood_items(Generator.item_amount()) is the item equivalent of Level.flood_monsters(Generator.being_weight()).

    else --quitting
        if Level.result() == 1 then
            ui.msg("The voice booms, \"Coward!\" You hear screams everywhere! " ..
                   "\"Coward! Coward! COWARD!\"")
        elseif Level.result() == 2 then
            ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" " ..
                   "You hear screams everywhere! \"Boooo...\"")
        elseif Level.result() < 10 then
            ui.msg("The voice booms, \"An impressive run, Mortal!  We appreciate " ..
                   "it!\" The crowd starts to chant! \"Encore! Encore!\"")
        else
            ui.msg("\"Ladies and gentlemen, your champion, "
                   .. Player.get_name() .. ". He survived " .. Level.result() ..
                   " rounds in our arena! That has to be some sort of record. " ..
                   "Give him a hand folks!\" The crowd starts to chant your name " ..
                   "and they begin throwing items into the ring!")
            Level.flood_items(Generator.item_weight())
        end
    end
end

Upon choosing not to continue, the announcer displays a variety of messages, although they are few enough (and specific enough) not to create another array for them. (They CAN be tabulated, but it is less bulky than for the other cases.) If the player survived at least ten waves before stopping, the crowd "throws items onto the stage" and you get some pointless gear when you're done, just as before.

.OnExit()

function inf_arena.OnExit()
    if Level.result() < 10 then
        ui.msg("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
    else
        ui.msg("The voice laughs, \"Remember to come back once you return to Hell " ..
               "for the extended stay\"")
    end
    --used in .OnMortem()
    arena.result = "had enough of the gauntlet at wave "..Level.result()
end

OnExit() has only gained a cosmetic change in the case where the player survived more than ten waves before retreating.

.OnMortem()

function arena.OnMortem()
    local kill = player.killedby --calls kill descriptions from beings
    if arena.result then kill = arena.result end
        player:mortem_print( " "..player.name..", level "..player.explevel.." "
                             .." "..klasses[player.klass].name..", "..kill )
        --e.g., "Cool Guy, level 1 Marine, had enough of the gauntlet at wave 8"
        player:mortem_print(" in the Infinite Arena...")
end

As with OnExit(), OnMortem() only changes some of the text in the mortem depending on how the player ends the game.

Review

The following is the entire source code for the Infinite Arena.

core.declare("inf_arena", {} )
 
--enemy scaling for each wave: note that game difficulty will modify scaling independently
local mob_scale_factor = 0.5
--customize min_lev/max_lev/weight of each enemy (defaults given)
--for names of beings, check Object IDs in the modding documentation
--be aware that JC auto-ends the game unless modified directly
local mobGenStats = {
    { being = "former",         min_lev = 0,   max_lev = 12,  weight = 10 },
    { being = "sergeant",       min_lev = 2,   max_lev = 15,  weight = 10 },
    { being = "captain",        min_lev = 8,   max_lev = 15,  weight = 10 },
    { being = "imp",            min_lev = 0,   max_lev = 17,  weight = 8  },
    { being = "demon",          min_lev = 4,   max_lev = 20,  weight = 6  },
    { being = "lostsoul",       min_lev = 6,   max_lev = 16,  weight = 10 },
    { being = "knight",         min_lev = 9,   max_lev = 15,  weight = 6  },
    { being = "cacodemon",      min_lev = 10,  max_lev = 50,  weight = 6  },
    { being = "commando",       min_lev = 12,  max_lev = 17,  weight = 6  },
    { being = "pain",           min_lev = 12,  max_lev = 17,  weight = 2  },
    { being = "baron",          min_lev = 12,  max_lev = 200, weight = 6  },
    { being = "arachno",        min_lev = 13,  max_lev = 50,  weight = 4  },
    { being = "revenant",       min_lev = 13,  max_lev = 200, weight = 5  },
    { being = "mancubus",       min_lev = 15,  max_lev = 200, weight = 7  },
    { being = "arch",           min_lev = 16,  max_lev = 200, weight = 4  },
    { being = "nimp",           min_lev = 30,  max_lev = 60,  weight = 8  },
    { being = "ndemon",         min_lev = 40,  max_lev = 200, weight = 6  },
    { being = "ncacodemon",     min_lev = 51,  max_lev = 200, weight = 6  },
    { being = "narachno",       min_lev = 50,  max_lev = 200, weight = 5  },
    { being = "narch",          min_lev = 90,  max_lev = 200, weight = 3  },
    { being = "bruiser",        min_lev = 50,  max_lev = 200, weight = 6  },
    { being = "lava_elemental", min_lev = 70,  max_lev = 200, weight = 1  },
    { being = "shambler",       min_lev = 80,  max_lev = 200, weight = 3  },
    { being = "agony",          min_lev = 80,  max_lev = 200, weight = 1  },
    { being = "cyberdemon",     min_lev = 80,  max_lev = 200, weight = 1  },
    { being = "arenamaster",    min_lev = 0,   max_lev = 0,   weight = 0  },
    { being = "angel",          min_lev = 0,   max_lev = 0,   weight = 0  },
    { being = "jc",             min_lev = 0,   max_lev = 0,   weight = 0  },
}
 
--item scaling for each wave
local item_scale_factor = 0.5
--customize level/weight of items (defaults given)
--for names of items, check Object IDs in the modding documentation
local itemGenStats = {
    --Commons
 
    --weapons
    { item = "knife",        level = 1,  weight = 640 },
    { item = "pistol",       level = 1,  weight = 70  },
    { item = "shotgun",      level = 2,  weight = 180 },
    { item = "ashotgun",     level = 2,  weight = 160 },
    { item = "dshotgun",     level = 4,  weight = 100 },
    { item = "chaingun",     level = 5,  weight = 200 },
    { item = "bazooka",      level = 7,  weight = 200 },
    { item = "plasma",       level = 12, weight = 70  },
    --ammo/ammo packs
    { item = "ammo",         level = 1,  weight = 500 },
    { item = "pammo",        level = 3,  weight = 700 },
    { item = "shell",        level = 2,  weight = 400 },
    { item = "pshell",       level = 4,  weight = 200 },
    { item = "rocket",       level = 5,  weight = 60  },
    { item = "procket",      level = 7,  weight = 60  },
    { item = "cell",         level = 8,  weight = 36  },
    { item = "pcell",        level = 10, weight = 18  },
    --armor/boots
    { item = "garmor",       level = 1,  weight = 400 },
    { item = "barmor",       level = 4,  weight = 240 },
    { item = "rarmor",       level = 7,  weight = 150 },
    { item = "sboots",       level = 4,  weight = 240 },
    { item = "pboots",       level = 7,  weight = 150 },
    { item = "psboots",      level = 11, weight = 80  },
    --consumables
    { item = "smed",         level = 1,  weight = 600 },
    { item = "lmed",         level = 5,  weight = 400 },
    { item = "phase",        level = 5,  weight = 200 },
    { item = "hphase",       level = 7,  weight = 100 },
    { item = "epack",        level = 5,  weight = 100 },
    { item = "nuke",         level = 10, weight = 40  },
    { item = "lava_element", level = 23, weight = 0   },
    { item = "mod_power",    level = 7,  weight = 120 },
    { item = "mod_tech",     level = 6,  weight = 120 },
    { item = "mod_bulk",     level = 6,  weight = 120 },
    { item = "mod_agility",  level = 5,  weight = 120 },
    --powerups
    { item = "shglobe",      level = 1,  weight = 900 },
    { item = "lhglobe",      level = 6,  weight = 330 },
    { item = "scglobe",      level = 4,  weight = 150 },
    { item = "bpack",        level = 1,  weight = 200 },
    { item = "iglobe",       level = 7,  weight = 200 },
    { item = "msglobe",      level = 16, weight = 60  },
    { item = "map",          level = 1,  weight = 200 },
    { item = "pmap",         level = 1,  weight = 80  },
    { item = "ashard",       level = 5,  weight = 700 },
    { item = "backpack",     level = 7,  weight = 0   },
 
    --Exotics
 
    --weapons
    { item = "chainsaw",        level = 12, weight = 3  },
    { item = "ublaster",        level = 8,  weight = 2  },
    { item = "ucpistol",        level = 4,  weight = 6  },
    { item = "uashotgun",       level = 6,  weight = 6  },
    { item = "upshotgun",       level = 12, weight = 4  },
    { item = "udshotgun",       level = 10, weight = 5  },
    { item = "uminigun",        level = 10, weight = 6  },
    { item = "umbazooka",       level = 10, weight = 6  },
    { item = "unapalm",         level = 10, weight = 6  },
    { item = "ulaser",          level = 12, weight = 5  },
    { item = "unplasma",        level = 15, weight = 4  },
    { item = "utristar",        level = 12, weight = 4  },
    { item = "bfg9000",         level = 20, weight = 2  },
    { item = "unbfg9000",       level = 22, weight = 2  },
    { item = "utrans",          level = 14, weight = 3  },
    --armor/boots
    { item = "uoarmor",         level = 7,  weight = 4  },
    { item = "uparmor",         level = 10, weight = 6  },
    { item = "upboots",         level = 8,  weight = 6  },
    { item = "ugarmor",         level = 15, weight = 6  },
    { item = "ugboots",         level = 10, weight = 6  },
    { item = "umedarmor",       level = 5,  weight = 6  },
    { item = "uduelarmor",      level = 5,  weight = 6  },
    { item = "ubulletarmor",    level = 2,  weight = 4  },
    { item = "uballisticarmor", level = 2,  weight = 5  },
    { item = "ueshieldarmor",   level = 5,  weight = 3  },
    { item = "uplasmashield",   level = 10, weight = 3  },
    { item = "uenergyshield",   level = 8,  weight = 3  },
    { item = "ubalshield",      level = 6,  weight = 3  },
    { item = "uacidboots",      level = 8,  weight = 5  },
    --consumables
    { item = "uswpack",         level = 5,  weight = 10 },
    { item = "ubskull",         level = 5,  weight = 8  },
    { item = "ufskull",         level = 7,  weight = 8  },
    { item = "uhskull",         level = 9,  weight = 8  },
    { item = "umod_firestorm",  level = 10, weight = 4  },
    { item = "umod_sniper",     level = 10, weight = 4  },
    { item = "umod_nano",       level = 10, weight = 4  },
    { item = "umod_onyx",       level = 10, weight = 4  },
 
    --Uniques
 
    --weapons	
    { item = "ubutcher",     level = 1,  weight = 2 },
    { item = "spear",        level = 16, weight = 0 },
    { item = "uscythe",      level = 16, weight = 0 },
    { item = "udragon",      level = 16, weight = 1 },
    { item = "utrigun",      level = 8,  weight = 2 },
    { item = "ujackal",      level = 10, weight = 2 },
    { item = "uberetta",     level = 6,  weight = 3 },
    { item = "usjack",       level = 12, weight = 2 },
    { item = "urbazooka",    level = 12, weight = 2 },
    { item = "uacid",        level = 12, weight = 3 },
    { item = "urailgun",     level = 15, weight = 2 },
    { item = "ubfg10k",      level = 20, weight = 1 },
    --armor/boots
    { item = "umarmor",      level = 15, weight = 3 },
    { item = "ucarmor",      level = 10, weight = 2 },
    { item = "unarmor",      level = 10, weight = 3 },
    { item = "umedparmor",   level = 10, weight = 2 },
    { item = "ulavaarmor",   level = 12, weight = 2 },
    { item = "uenviroboots", level = 10, weight = 2 },
    { item = "ushieldarmor", level = 10, weight = 2 },
    { item = "uberarmor",    level = 10, weight = 1 },
    { item = "aarmor",       level = 22, weight = 0 },
    --consumables
    { item = "uhwpack",      level = 10, weight = 4 },
    { item = "umodstaff",    level = 15, weight = 4 },
    { item = "uarenastaff",  level = 4,  weight = 0 },
}
 
--changes the generation stats of enemies based on mobGenStats
for _,v in ipairs(mobGenStats) do
    beings[v.being].min_lev = v.min_lev
    beings[v.being].max_lev = v.max_lev
    beings[v.being].weight = v.weight
end
 
--changes the generation stats of items based on itemGenStats
for _,v in ipairs(itemGenStats) do
    items[v.item].level  = v.level
    items[v.item].weight = v.weight
end
 
--add your own announcer lines here
local anncrMsg = {
    --displays whenever player completes wave
    success = {
        {
        "The voice booms, \"Congratulations mortal!",
        "The voice booms, \"Impressive mortal!",
        "The voice booms, \"Most impressive.",
        "The voice booms, \"You are a formidable warrior!",
        },
        {
        "Each of your triumphs is a work of art!",
        "Your ability to survive is incredible!",
        "You've given us a great show!",
        "You would make a terrific hell warrior!",
        },
        {
        "But can you keep going?\"",
        "How much longer can you go?\"",
        "Will you fight with us a little more?\"",
        "I can let you go now if you like, or...\"",
        }
    },
    --displays whenever player decides to continue to another wave
    choice = {
        {
        "The voice booms, \"I like it! Let the show go on!\"",
        "The voice booms, \"Excellent! May the fight begin!!!\"",
        },
        {
        "You hear screams everywhere! \"More Blood! More BLOOD!\"",
        "You hear screams everywhere! \"Kill, Kill, KILL!\"",
        }
    }
}
 
--add your own crowd lines here
--displays whenever enemy is killed
local crowdMsg = {
    "The crowd goes wild! \"BLOOD! BLOOD!\"",
    "The crowd cheers! \"Blood! Blood!\"",
    "The crowd cheers! \"Kill! Kill!\"",
    "The crowd hisses. \"We came for a REAL fight!\"",
    "The crowd boos. \"No skill, no kill!\"",
}
 
local function build_announcerMsg(MSG)
    for _,msgNum in ipairs(MSG) do
        ui.msg(table.random_pick(msgNum))
    end
end
 
--from Skulltag Arena, credit goes to yaflhdztioxo
--initializes table of cells that contains only corpses
local corpseCells = {}
 
local function get_corpses()
    for i = 1, #cells do
        if cells[i] and cells[i].flags then
            if cells[i].flag_set[CF_CORPSE] == true then
                table.insert(corpseCells, i) --i == sID's numeric value
            end
        end
    end
end
 
--removes corpses and blood-like cells
local function clear_corpses()
    --fade away all blood
    Generator.transmute("blood", "floor")
    Generator.transmute("bloodpool", "blood")
    --changes corpses to blood
    for i = 1, #corpseCells do
        Generator.transmute(corpseCells[i], "bloodpool")
    end
end
 
--changes player equipment
--add "nil" for any slot that should be empty
--always refreshes all slots: do not use to change only one equipment slot
local function build_gear(weap,prep,body,foot)
    player.eq:clear()
    if weap then player.eq.weapon   = item.new(weap) end --main weapon
    if prep then player.eq.prepared = item.new(prep) end --prepared weapon	
    if body then player.eq.armor    = item.new(body) end --body armor
    if foot then player.eq.boots    = item.new(foot) end --boots
end
 
--changes player inventory
--always refreshes all slots: do not use to change only one equipment slot
--[[
input is an array of tables with two fields:
-name is the identifier of the item that goes in the inventory
-amt is how many items should be added (or how many units of ammo)
example table (based on default inventory):
 
local starter_pack = {
    {name = "ammo", amt = 24},
    {name = "smed", amt = 2},
}
--]]
local function build_pack(itemPack)
    player.inv:clear()
    for _,part in ipairs(itemPack) do
        if items[part.name].type == ITEMTYPE_AMMO then
            --ammo is handled separately, loops for however many stacks are needed
            for n=1,math.ceil(part.amt/items[part.name].ammomax) do
                local pack = item.new(part.name)
                if part.amt < pack.ammomax then
                    --remainder (or single) stack
                    pack.ammo = part.amt
                else
                    --filled (and/or multiple) stacks
                    pack.ammo = pack.ammomax
                    part.amt = part.amt - pack.ammomax
                end
                player.inv:add(pack)
            end
        else
            --loops for however many items are needed
            for n=1,part.amt do
                player.inv:add(part.name)
            end
        end
    end
end
 
 
 
function inf_arena.OnEnter()
    --inventory table used for build_pack()
    local itemPack = {
        {name = "smed",  amt = 2},
        {name = "shell", amt = 50},
    }
    --set up starting eq/inv
    build_gear("shotgun","pistol",nil,nil)
    build_pack(itemPack)
 
    --Print announcer messages
    ui.msg("A devilish voice announces: \"Welcome to Hell's Arena, mortal! " ..
           "\"You are either very brave or very foolish. Either way I like it! " ..
           "\"And so do the crowds!\" Suddenly you hear screams everywhere! " ..
           "\"Blood! Blood! BLOOD!\" \"Kill all enemies and I shall reward thee!\"")
 
    --spawn first wave
    Level.flood_monsters(Generator.being_weight()*mob_scale_factor)
    Level.result(1)
end
 
function inf_arena.OnKill(being)
    --random message from crowdMsg array
    ui.msg(table.random_pick(crowdMsg))
end
 
function inf_arena.OnKillAll()
    --print more announcer stuff
    if Level.result() == 1 then
        ui.msg("The voice booms, \"Not bad mortal! For a weakling that you are, " ..
               "you show some determination.\" You hear screams everywhere! " ..
               "\"More Blood! More BLOOD!\" The voice continues, \"I can now " ..
               "let you go free, or you may try to complete the challenge!\"")
    elseif Level.result() == 2 then
        ui.msg("The voice booms, \"Impressive mortal! Your determination to " ..
               "survive makes me excited!\" You hear screams everywhere! " ..
               "\"More Blood! More BLOOD!\" \"I can let you go now, and give you " ..
               "a small reward, or you can choose to fight an additional challenge!\"")
    else
        --random message from anncrMsg.success array
        build_announcerMsg(anncrMsg.success)
    end
 
    local choice = ui.msg_confirm("Round " .. Level.result() + 1 .. " awaits. " ..
                                  "Do you want to continue the fight?")
    if choice == true then --continuing
        --random message from anncrMsg.choice array
        build_announcerMsg(anncrMsg.choice)
        --set up the danger level for the next wave
        Level.result(Level.result() + 1);
        Level.danger_level = Level.result();
        --spawn items for next wave
        Level.flood_items(Generator.item_amount()*item_scale_factor)
        --spawn enemies for next wave
        Level.flood_monsters(Generator.being_weight()*mob_scale_factor)
 
    else --quitting
        if Level.result() == 1 then
            ui.msg("The voice booms, \"Coward!\" You hear screams everywhere! " ..
                   "\"Coward! Coward! COWARD!\"")
        elseif Level.result() == 2 then
            ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" " ..
                   "You hear screams everywhere! \"Boooo...\"")
        elseif Level.result() < 10 then
            ui.msg("The voice booms, \"An impressive run, Mortal!  We appreciate " ..
                   "it!\" The crowd starts to chant! \"Encore! Encore!\"")
        else
            ui.msg("\"Ladies and gentlemen, your champion, "
                   .. Player.get_name() .. ". He survived " .. Level.result() ..
                   " rounds in our arena! That has to be some sort of record. " ..
                   "Give him a hand folks!\" The crowd starts to chant your name " ..
                   "and they begin throwing items into the ring!")
            Level.flood_items(Generator.item_weight())
        end
    end
end
 
function inf_arena.OnExit()
    if Level.result() < 10 then
        ui.msg("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
    else
        ui.msg("The voice laughs, \"Remember to come back once you return to Hell " ..
               "for the extended stay\"")
    end
    --used in .OnMortem()
    inf_arena.result = "had enough of the gauntlet at wave "..Level.result()
end
 
function inf_arena.OnMortem()
    local kill = player.killedby --calls kill descriptions from beings
    if inf_arena.result then kill = inf_arena.result end
        player:mortem_print( " "..player.name..", level "..player.explevel.." "
                             .." "..klasses[player.klass].name..", "..kill )
        --e.g., "Cool Guy, level 1 Marine, had enough of the gauntlet at wave 8"
        player:mortem_print(" in the Infinite Arena...")
end
 
--Creation of Infinite Arena
function inf_arena.run()
    Level.name = "Infinite Arena"
    Level.name_number = 0
    Level.fill("rwall")
 
    local translation = {
    ["."] = "floor",
    ["#"] = {"rwall", LFPERMANENT = true},
    ["X"] = "rwall",
    [","] = "blood",
    [">"] = "stairs",
    }
 
    local map = [[
#######################.............................########################
###########.....................................................############
#####..................................................................#####
##........................................................................##
#..........................................................................#
....,.......................................................................
.................................,...,......................................
.,.....................................,....................................
..,>.,..........................,...........................................
.,..,.................................,.,...................................
................................,..,...,....................................
............................................................................
............................................................................
#..........................................................................#
##........................................................................##
#####..................................................................#####
###########.....................................................############
#######################.............................########################
]]
    --change up the column types
    local column = {
    [[
,..,.,
,XXXX.
.X##X,
.XXXX.
,..,.,
    ]],
    [[
,..,.,
,X##X.
.####,
.X##X.
,..,.,
    ]],
    [[
,..,.,
,####.
.####,
.####.
,..,.,
    ]]
    }
 
    Level.place_tile(translation,map,2,2)
    for i=1,11 + math.random(4) do
        Level.scatter_put( area.new(5,3,68,15), translation, table.random_pick(column), "floor", 1)
    end
    Level.scatter(area.FULL_SHRINKED, "floor", "blood", 100)
    Level.player(38, 10)
end
Personal tools