Difference between revisions of "Modding:Tutorial/The Infinite Arena"
From DoomRL Wiki
Game Hunter (Talk | contribs) m (added source template) |
Game Hunter (Talk | contribs) m (→Review: new link) |
||
(7 intermediate revisions by one user not shown) | |||
Line 70: | Line 70: | ||
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). | 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): | + | 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''): |
<source lang="lua"> | <source lang="lua"> | ||
Line 79: | Line 79: | ||
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_table[small_table1].key1 --identical to above | ||
− | big_table[small_table1[key1 | + | big_table[small_table1][key1] --identical to above |
big_array.1 --returns the contents of the first array in big_array | big_array.1 --returns the contents of the first array in big_array | ||
Line 87: | Line 87: | ||
big_array.2[2] --identical to above | big_array.2[2] --identical to above | ||
big_array[2].2 --identical to above | big_array[2].2 --identical to above | ||
− | big_array[2[2 | + | big_array[2][2] --identical to above |
</source> | </source> | ||
Line 95: | Line 95: | ||
<source lang="lua"> | <source lang="lua"> | ||
− | local function | + | local function sum_diff(arg1,arg2) |
val1 = arg1+arg2 | val1 = arg1+arg2 | ||
val2 = arg1-arg2 | val2 = arg1-arg2 | ||
Line 106: | Line 106: | ||
</source> | </source> | ||
− | *First you must give the function a name: the above example is called | + | *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 | + | *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 | + | *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: | *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, | + | **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. | **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. | ||
Line 119: | Line 119: | ||
local num2 = 4 | local num2 = 4 | ||
− | local sum,diff = | + | local sum,diff = sum_diff(num1,num2) |
</source> | </source> | ||
− | The | + | 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. | 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. | ||
Line 132: | Line 132: | ||
====mobGenStats==== | ====mobGenStats==== | ||
<source lang="lua"> | <source lang="lua"> | ||
− | + | --customize min_lev/max_lev/weight of each enemy (defaults given) | |
− | + | ||
− | --customize | + | |
--for names of beings, check Object IDs in the modding documentation | --for names of beings, check Object IDs in the modding documentation | ||
--be aware that JC auto-ends the game unless modified directly | --be aware that JC auto-ends the game unless modified directly | ||
local mobGenStats = { | local mobGenStats = { | ||
− | { being = "former", | + | { being = "former", min_lev = 0, max_lev = 12, weight = 10 }, |
− | { being = "sergeant", | + | { being = "sergeant", min_lev = 2, max_lev = 15, weight = 10 }, |
− | { being = "captain", | + | { being = "captain", min_lev = 8, max_lev = 15, weight = 10 }, |
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | ''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 ''min_lev'', ''max_lev'', 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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | beings.former.weight = 0 | ||
+ | </source> | ||
+ | |||
+ | 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: | ||
<source lang="lua"> | <source lang="lua"> | ||
--changes the generation stats of enemies based on mobGenStats | --changes the generation stats of enemies based on mobGenStats | ||
for _,v in ipairs(mobGenStats) do | for _,v in ipairs(mobGenStats) do | ||
− | beings[v.being]. | + | beings[v.being].min_lev = v.min_lev |
− | beings[v.being]. | + | beings[v.being].max_lev = v.max_lev |
beings[v.being].weight = v.weight | beings[v.being].weight = v.weight | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | 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 ''min_lev'', ''max_lev'', 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. | ||
+ | |||
+ | <source lang="lua"> | ||
+ | --enemy scaling for each wave: note that game difficulty will modify scaling independently | ||
+ | local mob_scale_factor = 0.5 | ||
+ | </source> | ||
+ | |||
+ | 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==== | ====itemGenStats==== | ||
<source lang="lua"> | <source lang="lua"> | ||
− | |||
− | |||
--customize level/weight of items (defaults given) | --customize level/weight of items (defaults given) | ||
--for names of items, check Object IDs in the modding documentation | --for names of items, check Object IDs in the modding documentation | ||
Line 191: | Line 187: | ||
{ item = "pistol", level = 1, weight = 70 }, | { item = "pistol", level = 1, weight = 70 }, | ||
{ item = "shotgun", level = 2, weight = 180 }, | { item = "shotgun", level = 2, weight = 180 }, | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--ammo/ammo packs | --ammo/ammo packs | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--armor/boots | --armor/boots | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--consumables | --consumables | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--powerups | --powerups | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--Exotics | --Exotics | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
--Uniques | --Uniques | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | ''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.) | ||
<source lang="lua"> | <source lang="lua"> | ||
Line 315: | Line 212: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <source lang="lua"> | ||
+ | --item scaling for each wave | ||
+ | local item_scale_factor = 0.5 | ||
+ | </source> | ||
+ | |||
+ | 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==== | ====anncrMsg and crowdMsg==== | ||
Line 330: | Line 236: | ||
}, | }, | ||
{ | { | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
}, | }, | ||
{ | { | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
} | } | ||
}, | }, | ||
--displays whenever player decides to continue to another wave | --displays whenever player decides to continue to another wave | ||
choice = { | choice = { | ||
− | + | .... | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | ''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()'''. | ||
<source lang="lua"> | <source lang="lua"> | ||
Line 367: | Line 264: | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | 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=== | ===Functions=== | ||
Line 383: | Line 282: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''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()==== | ====build_pack()==== | ||
Line 401: | Line 304: | ||
--filled (and/or multiple) stacks | --filled (and/or multiple) stacks | ||
pack.ammo = pack.ammomax | pack.ammo = pack.ammomax | ||
+ | part.amt = part.amt - pack.ammomax | ||
end | end | ||
player.inv:add(pack) | player.inv:add(pack) | ||
Line 414: | Line 318: | ||
</source> | </source> | ||
− | ==== | + | '''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 for 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()''': | ||
+ | |||
+ | <source lang="lua"> | ||
+ | local itemPack = { | ||
+ | {name = "chaingun", amt = 1 } | ||
+ | {name = "ammo", amt = 360} | ||
+ | {name = "smed", amt = 5 } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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()==== | ||
<source lang="lua"> | <source lang="lua"> | ||
local function build_announcerMsg(MSG) | local function build_announcerMsg(MSG) | ||
Line 422: | Line 353: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | 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()==== | ====get_corpses() and clear_corpses()==== | ||
<source lang="lua"> | <source lang="lua"> | ||
− | --initializes table of cells that | + | --initializes table of cells that only contains corpses |
local corpseCells = {} | local corpseCells = {} | ||
− | local function get_corpses | + | local function get_corpses() |
for i = 1, #cells do | for i = 1, #cells do | ||
if cells[i] and cells[i].flags then | if cells[i] and cells[i].flags then | ||
Line 438: | Line 371: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''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. | ||
<source lang="lua"> | <source lang="lua"> | ||
Line 447: | Line 382: | ||
--changes corpses to blood | --changes corpses to blood | ||
for i = 1, #corpseCells do | for i = 1, #corpseCells do | ||
− | Generator.transmute( | + | Generator.transmute(corpseCells[i], "bloodpool") |
end | end | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''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== | ==Engine Hooks== | ||
Line 460: | Line 397: | ||
Level.name = "Infinite Arena" | Level.name = "Infinite Arena" | ||
Level.name_number = 0 | Level.name_number = 0 | ||
− | Level.fill(" | + | Level.fill("rwall") |
+ | Generator.set_permanence(area.FULL) | ||
local translation = { | local translation = { | ||
["."] = "floor", | ["."] = "floor", | ||
− | ["#"] = " | + | ["#"] = {"rwall", LFPERMANENT = true}, |
− | ["X"] = " | + | ["X"] = "rwall", |
[","] = "blood", | [","] = "blood", | ||
[">"] = "stairs", | [">"] = "stairs", | ||
Line 523: | Line 461: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | 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()=== | ===.OnEnter()=== | ||
Line 547: | Line 487: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''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()=== | ===.OnKill()=== | ||
Line 555: | Line 499: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''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()=== | ===.OnKillAll()=== | ||
Line 574: | Line 520: | ||
build_announcerMsg(anncrMsg.success) | build_announcerMsg(anncrMsg.success) | ||
end | end | ||
− | + | .... | |
+ | </source> | ||
+ | '''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). | ||
+ | |||
+ | <source lang="lua"> | ||
+ | .... | ||
local choice = ui.msg_confirm("Round " .. Level.result() + 1 .. " awaits. " .. | local choice = ui.msg_confirm("Round " .. Level.result() + 1 .. " awaits. " .. | ||
"Do you want to continue the fight?") | "Do you want to continue the fight?") | ||
Line 587: | Line 538: | ||
--spawn enemies for next wave | --spawn enemies for next wave | ||
Level.flood_monsters(Generator.being_weight()*mob_scale_factor) | Level.flood_monsters(Generator.being_weight()*mob_scale_factor) | ||
− | + | .... | |
+ | </source> | ||
+ | |||
+ | 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())'''. | ||
+ | |||
+ | <source lang="lua"> | ||
else --quitting | else --quitting | ||
if Level.result() == 1 then | if Level.result() == 1 then | ||
Line 609: | Line 565: | ||
end | end | ||
</source> | </source> | ||
+ | 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()=== | ===.OnExit()=== | ||
Line 620: | Line 577: | ||
end | end | ||
--used in .OnMortem() | --used in .OnMortem() | ||
− | + | inf_arena.result = "had enough of the gauntlet at wave "..Level.result() | |
end | end | ||
</source> | </source> | ||
+ | |||
+ | '''OnExit()''' has only gained a cosmetic change in the case where the player survived more than ten waves before retreating. | ||
===.OnMortem()=== | ===.OnMortem()=== | ||
<source lang="lua"> | <source lang="lua"> | ||
− | function | + | function inf_arena.OnMortem() |
local kill = player.killedby --calls kill descriptions from beings | local kill = player.killedby --calls kill descriptions from beings | ||
− | if | + | if inf_arena.result then kill = inf_arena.result end |
player:mortem_print( " "..player.name..", level "..player.explevel.." " | player:mortem_print( " "..player.name..", level "..player.explevel.." " | ||
.." "..klasses[player.klass].name..", "..kill ) | .." "..klasses[player.klass].name..", "..kill ) | ||
Line 635: | Line 594: | ||
end | end | ||
</source> | </source> | ||
+ | |||
+ | As with '''OnExit()''', '''OnMortem()''' only changes some of the text in the mortem depending on how the player ends the game. | ||
==Review== | ==Review== | ||
+ | The following is the entire source code for the Infinite Arena. | ||
{{:Modding:Tutorial/The_Infinite_Arena/Source}} | {{:Modding:Tutorial/The_Infinite_Arena/Source}} | ||
+ | |||
+ | Source data: [http://dl.dropbox.com/u/54818507/inf_arena.module.zip inf_arena.module] |
Latest revision as of 16:49, 8 April 2012
(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 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 }, .... }
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 min_lev, max_lev, 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].min_lev = v.min_lev beings[v.being].max_lev = v.max_lev 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 min_lev, max_lev, 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 for 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("rwall") Generator.set_permanence(area.FULL) 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
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() inf_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 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
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
Source data: inf_arena.module