Modding:Tutorial/The Infinite Arena

From DoomRL Wiki

Revision as of 16:55, 3 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 add_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 "add_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 = add_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 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 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 are an array of strings. 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_announcement().

--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
                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. 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_announcement()

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

The build_announcement() 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_announcement(anncrMsg.success) sets up three random messages from each array in its array, and build_announcement(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 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

.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

.OnKill()

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

.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
 
    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

.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

.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

Review

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