Difference between revisions of "Modding:Tutorial/Recreating Hell's Arena"

From DoomRL Wiki

Jump to: navigation, search
m (Review: whoops forgot the .OnEnter())
m (Review: new link)
 
(7 intermediate revisions by one user not shown)
Line 12: Line 12:
 
**music files go into "music"
 
**music files go into "music"
 
*Whenever you want to access one of these sound or music files, you will use one of the following methods:
 
*Whenever you want to access one of these sound or music files, you will use one of the following methods:
**Sounds are most commonly called using Being:play_sound("filename"), where Being is whomever you want the sound to originate from. For "surround" sounds, use player:play_sound().
+
**Sounds are most commonly called using '''being:play_sound'''(''filename''), where "being" is whomever you want the sound to originate from. For "surround" sounds, use '''player:play_sound()'''.
**If you want the sound at a very specific point on the map, use [[Modding:Level#level_explosion|Level.explosion()]] and set the radius, delay, and damage of the explosion to zero: this removes any physical part of the explosion, allowing you to play only a sound at that point.
+
**If you want the sound at a very specific point on the map, use '''[[Modding:Level#level_explosion|Level.explosion()]]''' and set the radius, delay, and damage of the explosion to zero: this removes any physical part of the explosion, allowing you to play only a sound at that point.
**Music is added using core.play_music("filename"). It will override any music already playing. (Unfortunately there is no way to turn off the music: if you want no music to play, create a music file that has no audio and use it instead.)
+
**Music is added using '''core.play_music'''(''filename''). It will override any music already playing. (Unfortunately there is no way to turn off the music: if you want no music to play, create a music file that has no audio and use it instead.)
*'''Do not modify sound.lua or music.lua.''' This is technically a way to modify (or include) sounds and music for particular objects/calls, but neither of these files are included in the source folder or WAD file. For the sake of players wanting an easy dump into their DoomRL folder, it is higly recommended that you stick to the aforementioned procedure instead.
+
*<u>Do not modify sound.lua or music.lua.</u> This is technically a way to modify (or include) sounds and music for particular objects/calls, but neither of these files are included in the source folder or WAD file. For the sake of players wanting an easy dump into their DoomRL folder, it is higly recommended that you stick to the aforementioned procedure instead.
  
 
In the Hell's Arena module, Skulltag sounds are added at the start of the map, as well as the start of each wave. Music by Simon Volpert, named "Rounds of Hell", is also included at the beginning of the map and changes for each wave. (Adding sounds to beings is a lot more complicated, and we won't get into it here. Suffice it to say that it may prove more trouble than it's worth, not to mention that default sounds cannot be overwritten.)
 
In the Hell's Arena module, Skulltag sounds are added at the start of the map, as well as the start of each wave. Music by Simon Volpert, named "Rounds of Hell", is also included at the beginning of the map and changes for each wave. (Adding sounds to beings is a lot more complicated, and we won't get into it here. Suffice it to say that it may prove more trouble than it's worth, not to mention that default sounds cannot be overwritten.)
Line 27: Line 27:
  
 
function arena.run()
 
function arena.run()
Level.name = "Hell Arena" --that IS what it's called
+
    Level.name = "Hell Arena"                   --that IS what it's called
        Leve.name_number = 0     --remove floor #
+
    Level.name_number = 0                       --remove floor #
Level.fill("prwall")     --ultimately fills border with indestructible red walls
+
    Level.fill("rwall")                         --immortal border tiles
local translation = {
+
    local translation = {
['.'] = "floor", --gray floor
+
        ['.'] = "floor",                         --gray floor
[','] = "blood", --red floor (blood)
+
        [','] = "blood",                         --red floor (blood)
['#'] = "rwall", --red wall (bloodstone)
+
        ['#'] = "rwall",                         --red wall (bloodstone)
['>'] = "stairs" --gray (normal) stairs
+
        ['>'] = "stairs"                         --gray (normal) stairs
 
}
 
}
 
+
        --create map cell string (76x18)
+
    --create map cell string (76x18)
 
local map = [[
 
local map = [[
 
#######################.............................########################
 
#######################.............................########################
Line 58: Line 58:
 
#######################.............................########################
 
#######################.............................########################
 
]]
 
]]
        --create pillar cell string (6x5)
+
    --create pillar cell string (6x5)
local column = [[
+
    local column = [[
 
,..,.,
 
,..,.,
 
,####.
 
,####.
Line 66: Line 66:
 
,..,.,
 
,..,.,
 
]]
 
]]
+
        --puts map in non-border area
+
    --puts map in non-border area
Level.place_tile( translation, map, 2, 2 )
+
    Level.place_tile( translation, map, 2, 2 )
        --adds random pillars and blood
+
    --adds random pillars and blood
Level.scatter_put( area.new(5,3,68,15), translation, column, "floor",9+math.random(8))
+
    Level.scatter_put( area.new(5,3,68,15), translation, column, "floor",9+math.random(8))
Level.scatter( area.FULL_SHRINKED,"floor","blood",100)
+
    Level.scatter( area.FULL_SHRINKED,"floor","blood",100)
        --inserts player
+
    --all walls become indestructible
Level.player(38,10)
+
    Generator.set_permanence(area.FULL)
 +
    --inserts player
 +
    Level.player(38,10)
 
end
 
end
 
</source>
 
</source>
If you recall in the tutorial [[Modding:Tutorial/Constructing a Map|Constructing a Map]], we used the starting coordinate (1,1) for the Level.place_tile() method, rather than (2,2). This is because we included the map's border tiles in our test map. It is, in fact, possible to customize the border of the map (there are some issues with non-cell objects on the border, so it is suggested you don't allow for them): however, if the border of the map is going to be uniform, then when you use Level.fill(), fill the level with the border tiles you want. Then, when you write out the map string, you can ignore their existence altogether (the dimensions of such a map string should be 76x18), and place the map string at the starting coordinate (2,2) instead.
+
If you recall in the tutorial [[Modding:Tutorial/Constructing a Map|Constructing a Map]], we used the starting coordinate (1,1) for the '''Level.place_tile()''' method, rather than (2,2). This is because we included the map's border tiles in our test map. It is, in fact, possible to customize the border of the map (there are some issues with non-cell objects on the border, so it is suggested you don't allow for them): however, if the border of the map is going to be uniform, then when you use '''Level.fill()''', fill the level with the border tiles you want. Then, when you write out the map string, you can ignore their existence altogether (the dimensions of such a map string should be 76x18), and place the map string at the starting coordinate (2,2) instead.
  
Additionally, we have another cell string on this map: a 6x5 group consisting of a pillar surrounded by floor tiles. Hell's Arena randomly scatters these tiles around using the Level.scatter_put() method. The arguments are as follows:
+
Additionally, we have another cell string on this map: a 6x5 group consisting of a pillar surrounded by floor tiles. Hell's Arena randomly scatters these tiles around using the '''Level.scatter_put()''' method. The arguments are as follows:
 
*Area in which the cell string can be placed
 
*Area in which the cell string can be placed
 
*character/map translation table (thus you should write this cell string in the same terms as your map's)
 
*character/map translation table (thus you should write this cell string in the same terms as your map's)
Line 87: Line 89:
 
In the example, a fairly large area is carved out for these pillars to be placed, defining the same translation as for the normal map, using the variable name of the cell string, replacing the "floor" cell, and will be added anywhere from 10-17 times. Note that the middle of the map uses "blood" floor tiles rather than "floor", so that pillar will never appear here.
 
In the example, a fairly large area is carved out for these pillars to be placed, defining the same translation as for the normal map, using the variable name of the cell string, replacing the "floor" cell, and will be added anywhere from 10-17 times. Note that the middle of the map uses "blood" floor tiles rather than "floor", so that pillar will never appear here.
  
Finally, after placing the pillars, blood floor files are randomly added to the floor using the Level.scatter() method. Level.scatter() is similar to Level.scatter_put() but has no translation/map arguments. Instead, it asks for the area affected, the cell_id that will replace, the cell_id being replaced, and the number of times it should be added. In the example, "floor" is replaced with "blood" 100 times on any non-border tile (area.FULL_SHRINKED is equivalent to area.new(2,2,77,19)).
+
Finally, after placing the pillars, blood floor files are randomly added to the floor using the '''Level.scatter()''' method. '''Level.scatter()''' is similar to '''Level.scatter_put()''' but has no translation/map arguments. Instead, it asks for the area affected, the cell_id that will replace, the cell_id being replaced, and the number of times it should be added. In the example, "floor" is replaced with "blood" 100 times on any non-border tile (''area.FULL_SHRINKED'' is equivalent to '''area.new'''(''2'',''2'',''77'',''19'')).
  
 
===.OnEnter()===
 
===.OnEnter()===
 
<source lang="lua">
 
<source lang="lua">
 
function arena.OnEnter()
 
function arena.OnEnter()
     core.play_music("rounds_of_hell_1")     --use some kick-ass music
+
     core.play_music("rounds_of_hell_part_1")         --use some kick-ass music
     player:play_sound("preparetofight")     --skulltag announcer!
+
     player:play_sound("preparetofight")               --skulltag announcer!
     player.eq.weapon = item.new( "shotgun" ) --give the man a shotty
+
     player.eq.weapon = item.new( "shotgun" )         --give the man a shotty
 
+
 
     --add 50 shotgun shells to inventory in a single "shell" item
 
     --add 50 shotgun shells to inventory in a single "shell" item
 
     local shells = item.new( "shell" )
 
     local shells = item.new( "shell" )
Line 103: Line 105:
 
</source>
 
</source>
  
The .OnEnter() hook begins in a way you'd tend to expect: special sound/music is added and the player's inventory is adjusted (specifically they are given a shotgun in-hand and fifty shells in the bank).
+
The '''OnEnter()''' hook begins in a way you'd tend to expect: special sound/music is added and the player's inventory is adjusted (specifically they are given a shotgun in-hand and fifty shells in the bank).
  
 
<source lang="lua">
 
<source lang="lua">
 
     ....
 
     ....
     --sets up first wave
+
     Level.result(1)                                  --sets up first wave
    Level.result(1)
+
 
+
 
     --big announcement stuff
 
     --big announcement stuff
 
     ui.msg("A devilish voice announces:")
 
     ui.msg("A devilish voice announces:")
Line 122: Line 123:
 
</source>
 
</source>
  
The next part of .OnEnter() is a series of ui.msg() methods to display text onto the player's top part of the screen. Sounds are added are relevant points of the monologue. Note that, although ui.msg_enter isn't being used, the player will have to press ENTER once two lines of text is fully displayed. Getting the timing of this with sounds can be tricky, so I would personally recommend that you use ui.msg_enter just before a sound is to be played.
+
The next part of '''OnEnter()''' is a series of '''ui.msg()''' methods to display text onto the player's top part of the screen. Sounds are added are relevant points of the monologue. Note that, although '''ui.msg_enter()''' isn't being used, the player will have to press ENTER once two lines of text is fully displayed. Getting the timing of this with sounds can be tricky, so I would personally recommend that you use '''ui.msg_enter()''' just before a sound is to be played.
  
Also included here is the Level.result() method. Level.result() can be used in two ways:
+
Also included here is the '''Level.result()''' method. '''Level.result()''' can be used in two ways:
  
*Calling without any arguments returns the current value of Level.result()
+
*Calling without any arguments returns the current value of '''Level.result()'''
*Calling with an integer argument sets Level.result() to that integer value
+
*Calling with an integer argument sets '''Level.result()''' to that integer value
  
 
This value can be used for a number of things, but it is most often associated with events occurring in the level.
 
This value can be used for a number of things, but it is most often associated with events occurring in the level.
Line 134: Line 135:
 
     ....
 
     ....
 
     --create the first wave
 
     --create the first wave
     Level.summon("demon",3)               --3 demons
+
     Level.summon("demon",3)                           --3 demons
     Level.summon("lostsoul",2)             --2 lost souls
+
     Level.summon("lostsoul",2)                       --2 lost souls
     Level.summon("cacodemon",DIFFICULTY-1) --0/1/2/3/4 cacodemons
+
     Level.summon("cacodemon",DIFFICULTY-1)           --0/1/2/3/4 cacodemons
 
end
 
end
 
</source>
 
</source>
  
Finally, .OnEnter() spawns us some enemies using the Level.summon() method. This is a very simple function: input the being_id you want to spawn and how many, and the game will randomly generate them anywhere on the map. To spawn enemies in a specific area, you will want to use Level.area_summon(), which includes an area argument.
+
Finally, '''OnEnter()''' spawns us some enemies using the '''Level.summon()''' method. This is a very simple function: input the being_id you want to spawn and how many, and the game will randomly generate them anywhere on the map. To spawn enemies in a specific area, you will want to use Level.area_summon(), which includes an area argument.
  
The number of cacodemons in the level depends on the difficulty, which can be called through a pre-defined global variable called "DIFFICULTY". On I'm Too Young To Die, DIFFICULTY is equal to 1; on Hey, Not Too Rough, DIFFICULTY is equal to 2; and so on and so forth.
+
The number of cacodemons in the level depends on the difficulty, which can be called through a pre-defined global variable called ''DIFFICULTY''. On I'm Too Young To Die, ''DIFFICULTY'' is equal to 1; on Hey, Not Too Rough, ''DIFFICULTY'' is equal to 2; and so on and so forth.
  
 
===.OnKill()===
 
===.OnKill()===
Line 155: Line 156:
 
</source>
 
</source>
  
The .OnKill() hook is used in Hell's Arena to add some crowd cheering. It is randomized between three messages using a temporary variable that changes every time another enemy dies (as the variable is re-defined each time the hook is triggered).
+
The '''OnKill()''' hook is used in Hell's Arena to add some crowd cheering. It is randomized between three messages using a temporary variable that changes every time another enemy dies (as the variable is re-defined each time the hook is triggered).
  
 
===.OnKillAll()===
 
===.OnKillAll()===
 
<source lang="lua">
 
<source lang="lua">
 
function arena.OnKillAll()
 
function arena.OnKillAll()
     if Level.result() == 1 then                 --if first wave completes
+
     if Level.result() == 1 then                       --if first wave completes
 
         --more talk
 
         --more talk
 
         ui.msg("The voice booms, \"Not bad mortal! For a weakling that you ")
 
         ui.msg("The voice booms, \"Not bad mortal! For a weakling that you ")
Line 167: Line 168:
 
         ui.msg("The voice continues, \"I can now let you go free, or")
 
         ui.msg("The voice continues, \"I can now let you go free, or")
 
         ui.msg("you may try to complete the challenge!\"");
 
         ui.msg("you may try to complete the challenge!\"");
 
+
 
         --player can choose to continue
 
         --player can choose to continue
 
         local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
 
         local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
         if choice then                           --player chose to continue
+
         if choice then                               --player chose to continue
             core.play_music("rounds_of_hell_2")  --second verse better than first
+
             core.play_music("rounds_of_hell_part_2")  --second verse better than first
 
             player:play_sound("two")
 
             player:play_sound("two")
 
             ui.msg("The voice booms, \"I like it! Let the show go on!\"")
 
             ui.msg("The voice booms, \"I like it! Let the show go on!\"")
 
             ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
 
             ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
 
+
             Level.drop("chaingun")               --sweet drop
+
             Level.drop("chaingun")                   --sweet drop
             Level.summon("demon",3)             --3 demons
+
             Level.summon("demon",3)                   --3 demons
             Level.summon("cacodemon",DIFFICULTY) --1/2/3/4/5 cacodemons
+
             Level.summon("cacodemon",DIFFICULTY)     --1/2/3/4/5 cacodemons
 
         --player chose to stop
 
         --player chose to stop
 
         else
 
         else
Line 187: Line 188:
 
</source>
 
</source>
  
.OnKillAll() contains the bulk of code in this map, since each time a wave of enemies is killed, a number of things change. The first condition required is to check which wave we're on using Level.result(). Recall that Level.result() is equal to 1 based on the .OnEnter() hook, so the first .OnKillAll() will trigger for this first case.
+
'''OnKillAll()''' contains the bulk of code in this map, since each time a wave of enemies is killed, a number of things change. The first condition required is to check which wave we're on using '''Level.result()'''. Recall that '''Level.result()''' is equal to 1 based on the '''OnEnter()''' hook, so the first '''OnKillAll()''' will trigger for this first case.
  
After another lengthy explanation, the player is given a y/n confirmation using the ui.msg_confirm() method. This returns a true/false value depending on the input of the player: in the example we assign this return value to "choice". Thus we can use this variable to determine what happens next:
+
After another lengthy explanation, the player is given a y/n confirmation using the '''ui.msg_confirm()''' method. This returns a true/false value depending on the input of the player: in the example we assign this return value to ''choice''. Thus we can use this variable to determine what happens next:
  
*In the event that the player chooses to accept with the "y" key (choice is set to true), then we get another music change, some displayed praise, and some items and beings added to the level.
+
*In the event that the player chooses to accept with the "y" key (''choice'' is set to true), then we get another music change, some displayed praise, and some items and beings added to the level.
*In the event that the player choosen to decline with the "n" key (choice is set to false), then we get a displayed insult.
+
*In the event that the player choosen to decline with the "n" key (''choice'' is set to false), then we get a displayed insult.
  
Level.drop() should be self-explanatory: is it the item-equivalent to Level.summon(). Both the Level.drop() and Level.summon() methods can have the number of items/beings omitted: if so, the default value of 1 is used.
+
'''Level.drop()''' should be self-explanatory: is it the item equivalent of '''Level.summon()'''. Both the '''Level.drop()''' and '''Level.summon()''' methods can have the number of items/beings omitted: if so, the default value of 1 is used.
  
 
<source lang="lua">
 
<source lang="lua">
 
     ....
 
     ....
     elseif Level.result() == 2 then             --if second wave completes
+
     elseif Level.result() == 2 then                   --if second wave completes
 
         --even more talk
 
         --even more talk
 
         ui.msg("The voice booms, \"Impressive mortal! Your determination")
 
         ui.msg("The voice booms, \"Impressive mortal! Your determination")
Line 205: Line 206:
 
         ui.msg("\"I can let you go now, and give you a small reward, or")
 
         ui.msg("\"I can let you go now, and give you a small reward, or")
 
         ui.msg("you can choose to fight the final challenge!\"")
 
         ui.msg("you can choose to fight the final challenge!\"")
 
+
 
--player can choose to continue
 
--player can choose to continue
 
         local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
 
         local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
         if choice then                         --player chosen to continue
+
         if choice then                           --player chosen to continue
             core.play_music("rounds_of_hell_3") --strongest music yet
+
             core.play_music("rounds_of_hell_part_3") --strongest music yet
 
             player:play_sound("three")
 
             player:play_sound("three")
 
             ui.msg("The voice booms, \"Excellent! May the fight begin!!!\"")
 
             ui.msg("The voice booms, \"Excellent! May the fight begin!!!\"")
 
             ui.msg("You hear screams everywhere! \"Kill, Kill, KILL!\"")
 
             ui.msg("You hear screams everywhere! \"Kill, Kill, KILL!\"")
 
+
 
             --some more sweet loot
 
             --some more sweet loot
 
             Level.drop("shell",4)
 
             Level.drop("shell",4)
 
             Level.drop("ammo",4)
 
             Level.drop("ammo",4)
 
             --spawns are a little more difficulty-dependent
 
             --spawns are a little more difficulty-dependent
             if DIFFICULTY <= 2 then             --ITYTD and HNTR
+
             if DIFFICULTY <= 2 then             --ITYTD and HNTR
                 Level.summon("cacodemon",DIFFICULTY+2)
+
                 Level.summon("cacodemon",DIFFICULTY+1)
             elseif DIFFICULTY == 3 then         --HMP
+
             elseif DIFFICULTY == 3 then         --HMP
 
                 Level.summon("knight",2)
 
                 Level.summon("knight",2)
             elseif DIFFICULTY >= 4 then         --UV and N!
+
             elseif DIFFICULTY >= 4 then         --UV and N!
 
                 Level.summon("baron",2)
 
                 Level.summon("baron",2)
 
             end
 
             end
         else                                   --player chose to stop
+
         else                                     --player chose to stop
 
             ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" ")
 
             ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" ")
 
             ui.msg("You hear screams everywhere! \"Boooo...\"")
 
             ui.msg("You hear screams everywhere! \"Boooo...\"")
 
+
 
           --pointless loot
 
           --pointless loot
 
           Level.drop("shell",3)  
 
           Level.drop("shell",3)  
Line 237: Line 238:
 
</source>
 
</source>
  
This part of the .OnKillAll() code shows for the case of Level.result equal to 2. It is roughly the same as the first case (different music/items/summons/messages aside), but with the following nontrivial changes:
+
This part of the '''OnKillAll()''' code shows for the case of '''Level.result()''' equal to 2. It is roughly the same as the first case (different music/items/summons/messages aside), but with the following nontrivial changes:
  
*The enemies used in the final wave are included in an if-then-elseif conditional statement, with each condition depending on DIFFICULTY. We have three settings: cacodemons (2 on ITYTD, 3 on HNTR), hell knights (on HMP), and barons of hell (on UV and N!).
+
*The enemies used in the final wave are included in an if-then-elseif conditional statement, with each condition depending on ''DIFFICULTY''. We have three settings: cacodemons (2 on ITYTD, 3 on HNTR), hell knights (on HMP), and barons of hell (on UV and N!).
 
*If the player chooses to decline, they will still receive some items. This is simply how it works in the vanilla map, and serves no real purpose in a single-level module such as this.
 
*If the player chooses to decline, they will still receive some items. This is simply how it works in the vanilla map, and serves no real purpose in a single-level module such as this.
  
 
<source lang="lua">
 
<source lang="lua">
 
     ....
 
     ....
     elseif Level.result() == 3 then           --if third wave completes
+
     elseif Level.result() == 3 then             --if third wave completes
 
         --finally done talking
 
         --finally done talking
 
         ui.msg("The voice booms, \"Congratulations mortal! A pity you came to")
 
         ui.msg("The voice booms, \"Congratulations mortal! A pity you came to")
Line 250: Line 251:
 
         ui.msg("\"I grant you the title of Hell's Arena Champion!\"")
 
         ui.msg("\"I grant you the title of Hell's Arena Champion!\"")
 
         ui.msg("\"And a promise is a promise... search the arena again...\"")
 
         ui.msg("\"And a promise is a promise... search the arena again...\"")
 
+
 
         --also pointless loot
 
         --also pointless loot
 
         Level.drop("scglobe")
 
         Level.drop("scglobe")
Line 259: Line 260:
 
</source>
 
</source>
  
Finally, .OnKillAll() checks for the case of Level.result equal to 3: this is after all three waves have been killed. The announcer finishes entirely and the player receives some superfluous rewards.
+
Finally, '''OnKillAll()''' checks for the case of Level.result equal to 3: this is after all three waves have been killed. The announcer finishes entirely and the player receives some superfluous rewards.
  
 
<source lang="lua">
 
<source lang="lua">
 
     ....
 
     ....
     Level.result(Level.result()+1) --increment level counter each time a wave completes
+
    --increment level counter each time a wave completes
 +
     Level.result(Level.result()+1)
 
end
 
end
 
</source>
 
</source>
  
This final piece of code is very important: each time .OnKillAll() is triggered, after all the Level.result checks are made, Level.reuslt() is incremented by one. This means that, after the first wave of enemies is killed, the case of Level.result equal to 1 is executed, the other cases are ignored, and then Level.result() is set to 2. This repeats for the other cases, albeit with different lines being executed before Level.result() is incremented.
+
This final piece of code is very important: each time '''OnKillAll()''' is triggered, after all the '''Level.result()''' checks are made, '''Level.result()''' is incremented by one. This means that, after the first wave of enemies is killed, the case of Level.result equal to 1 is executed, the other cases are ignored, and then Level.result() is set to 2. This repeats for the other cases, albeit with different lines being executed before Level.result() is incremented.
  
 
===.OnExit()===
 
===.OnExit()===
Line 273: Line 275:
 
function arena.OnExit()
 
function arena.OnExit()
 
     ui.msg_enter("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
 
     ui.msg_enter("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
 +
 
     --takes care of mortem text for "killed by something unknown..."
 
     --takes care of mortem text for "killed by something unknown..."
     local result = Level.result()
+
     if Level.result() < 4
    if result < 4
+
         then arena.result = "fled alive the trials at wave "..Level.result()
         then arena.result = "fled alive the trials at wave "..result
+
 
         else arena.result = "completed the trials"
 
         else arena.result = "completed the trials"
 
     end
 
     end
Line 282: Line 284:
 
</source>
 
</source>
  
Once the player leaves the level (and .OnExit() triggers), a final statement is given (which must be confirmed to continue, lest it be missed entirely) and a new field to "arena" is added, this time a string called "result" (which is different from the local variable "result" that is defined here as well). If Level.result() is less than 4 (which is to say, all waves were not killed) then arena.result writes out when the player left using the local "result". If Level.result() is 4 (or technically greater) then arena.result writes out that the player finished. But where is this string going to?
+
Once the player leaves the level (and '''OnExit()''' triggers), a final statement is given (which must be confirmed to continue, lest it be missed entirely) and a new field to ''arena'' is added, this time a string called ''result''. If '''Level.result()''' is less than 4 (which is to say, all waves were not killed) then ''arena.result'' writes out when the player left using '''Level.result()''' as the wave number. If '''Level.result()''' is 4 (or technically greater) then ''arena.result'' writes out that the player finished. But where is this string going to?
  
 
===.OnMortem()===
 
===.OnMortem()===
 
<source lang="lua">
 
<source lang="lua">
 
function arena.OnMortem()
 
function arena.OnMortem()
     local kill = player.killedby --calls kill descriptions from beings
+
     local kill = player.killedby                 --calls kill descriptions from beings
 
     if arena.result then kill = arena.result end
 
     if arena.result then kill = arena.result end
 
         player:mortem_print( " "..player.name..", level "..player.explevel.." "
 
         player:mortem_print( " "..player.name..", level "..player.explevel.." "
Line 293: Line 295:
 
         --e.g., "Cool Guy, level 1 Marine, fled alive the trials at wave 3"
 
         --e.g., "Cool Guy, level 1 Marine, fled alive the trials at wave 3"
 
         player:mortem_print(" in the Hell Arena...")
 
         player:mortem_print(" in the Hell Arena...")
    end
 
 
end
 
end
 
</source>
 
</source>
  
The .OnMortem() hook is called whenever the player has exited the level, immediately after .OnExit(), and can be used to change what is displayed on the mortem screen. Here we see arena.result in action:
+
The '''OnMortem()''' hook is called whenever the player has exited the level, immediately after '''OnExit()''', and can be used to change what is displayed on the mortem screen. Here we see ''arena.result'' in action:
  
*If the player is killed by an enemy, that being's kill_desc (or kill_desc_melee) can be called using player.killedby, which is then set to the local variable "kill". If not, "kill" will be empty when the first line executes.
+
*If the player is killed by an enemy, that being's ''kill_desc'' (or ''kill_desc_melee'') can be called using ''player.killedby'', which is then set to the local variable ''kill''. If not, ''kill'' will be empty when the first line executes.
*If arena.result was created (and will thus be true), then "kill" is instead set to that string. Since arena.result is only created if the player chooses to exit the game, this will not overwrite the case where the player is actually killed. It is done mostly to avoid any instances in which there would be a "killed by something unknown..." message.
+
*If ''arena.result'' was created (and will thus be true), then ''kill'' is instead set to that string. Since ''arena.result'' is only created if the player chooses to exit the game, this will not overwrite the case where the player is actually killed. It is done mostly to avoid any instances in which there would be a "killed by something unknown..." message.
  
Finally, the first two lines of the mortem are replaced with the player:mortem_print() method. player.name is the name of the player (as determined in-game); player.explevel is the character level; and klasses[player.klass].name checks the class prototype and returns the player's chosen class's name (for example, Marine).
+
Finally, the first two lines of the mortem are replaced with the '''player:mortem_print()''' method. ''player.name'' is the name of the player (as determined in-game); ''player.explevel'' is the character level; and ''klasses[player.klass].name'' checks the class prototype and returns the player's chosen class's name (for example, "Marine").
  
 
==Review==
 
==Review==
 
Here is the entire main.lua file to view at once:
 
Here is the entire main.lua file to view at once:
  
<source lang="lua">
+
{{:Modding:Tutorial/Recreating Hell's Arena/Source}}
--always declare the module_id global
+
core.declare( "arena" , {} )
+
 
+
function arena.OnEnter()
+
    core.play_music("rounds_of_hell_1")      --use some kick-ass music
+
    player:play_sound("preparetofight")      --skulltag announcer!
+
    player.eq.weapon = item.new( "shotgun" ) --give the man a shotty
+
 
+
    --add 50 shotgun shells to inventory in a single "shell" item
+
    local shells = item.new( "shell" )
+
    shells.ammo = 50
+
    player.inv:add( shells )
+
 
+
    Level.result(1)                          --sets up first wave
+
 
+
    --big announcement stuff
+
    ui.msg("A devilish voice announces:")
+
    ui.msg("\"Welcome to Hell's Arena, mortal!\"")
+
    ui.msg("\"You are either very foolish, or very brave. Either way I like it!\"")
+
    ui.msg("\"And so do the crowds!\"")
+
    player:play_sound("fight")
+
    ui.msg("Suddenly you hear screams everywhere! \"Blood! Blood! BLOOD!\"")
+
    ui.msg("The voice booms again, \"Kill all enemies and I shall reward thee!\"")
+
    player:play_sound("one")
+
 
+
    --create the first wave
+
    Level.summon("demon",3)                  --3 demons
+
    Level.summon("lostsoul",2)              --2 lost souls
+
    Level.summon("cacodemon",DIFFICULTY-1)  --0/1/2/3/4 cacodemons
+
end
+
 
+
function arena.OnKill()
+
        --randomized cheers from the crowd
+
local temp = math.random(3)
+
if    temp == 1 then ui.msg("The crowds go wild! \"BLOOD! BLOOD!\"")
+
elseif temp == 2 then ui.msg("The crowds cheer! \"Blood! Blood!\"")
+
else                  ui.msg("The crowds cheer! \"Kill! Kill!\"") end
+
end
+
 
+
function arena.OnKillAll()
+
    if Level.result() == 1 then                  --if first wave completes
+
        --more talk
+
        ui.msg("The voice booms, \"Not bad mortal! For a weakling that you ")
+
        ui.msg("are, you show some determination.\"");
+
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
+
        ui.msg("The voice continues, \"I can now let you go free, or")
+
        ui.msg("you may try to complete the challenge!\"");
+
 
+
        --player can choose to continue
+
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
+
        if choice then                          --player chose to continue
+
            core.play_music("rounds_of_hell_2")  --second verse better than first
+
            player:play_sound("two")
+
            ui.msg("The voice booms, \"I like it! Let the show go on!\"")
+
            ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
+
 
+
            Level.drop("chaingun")              --sweet drop
+
            Level.summon("demon",3)              --3 demons
+
            Level.summon("cacodemon",DIFFICULTY) --1/2/3/4/5 cacodemons
+
        --player chose to stop
+
        else
+
            ui.msg("The voice booms, \"Coward!\" ")
+
            ui.msg("You hear screams everywhere! \"Coward! Coward! COWARD!\"")
+
        end
+
 
+
    elseif Level.result() == 2 then              --if second wave completes
+
        --even more talk
+
        ui.msg("The voice booms, \"Impressive mortal! Your determination")
+
        ui.msg("to survive makes me excited!\"")
+
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
+
        ui.msg("\"I can let you go now, and give you a small reward, or")
+
        ui.msg("you can choose to fight the final challenge!\"")
+
 
+
--player can choose to continue
+
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
+
        if choice then                          --player chosen to continue
+
            core.play_music("rounds_of_hell_3")  --strongest music yet
+
            player:play_sound("three")
+
            ui.msg("The voice booms, \"Excellent! May the fight begin!!!\"")
+
            ui.msg("You hear screams everywhere! \"Kill, Kill, KILL!\"")
+
 
+
            --some more sweet loot
+
            Level.drop("shell",4)
+
            Level.drop("ammo",4)
+
            --spawns are a little more difficulty-dependent
+
            if DIFFICULTY <= 2 then              --ITYTD and HNTR
+
                Level.summon("cacodemon",DIFFICULTY+2)
+
            elseif DIFFICULTY == 3 then          --HMP
+
                Level.summon("knight",2)
+
            elseif DIFFICULTY >= 4 then          --UV and N!
+
                Level.summon("baron",2)
+
            end
+
        else                                    --player chose to stop
+
            ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" ")
+
            ui.msg("You hear screams everywhere! \"Boooo...\"")
+
 
+
          --pointless loot
+
          Level.drop("shell",3)
+
          Level.drop("lmed")
+
          Level.drop("smed")
+
        end
+
 
+
    elseif Level.result() == 3 then              --if third wave completes
+
        --finally done talking
+
        ui.msg("The voice booms, \"Congratulations mortal! A pity you came to")
+
        ui.msg("destroy us, for you would make a formidable hell warrior!\"")
+
        ui.msg("\"I grant you the title of Hell's Arena Champion!\"")
+
        ui.msg("\"And a promise is a promise... search the arena again...\"")
+
 
+
        --also pointless loot
+
        Level.drop("scglobe")
+
        Level.drop("barmor")
+
        Level.drop("lmed")
+
    end
+
 
+
    --increment level counter each time a wave completes
+
    Level.result(Level.result()+1)
+
end
+
 
+
function arena.OnExit()
+
    ui.msg_enter("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
+
    --takes care of mortem text for "killed by something unknown..."
+
    local result = Level.result()
+
    if result < 4
+
        then arena.result = "fled alive the trials at wave "..result
+
        else arena.result = "completed the trials"
+
    end
+
end
+
 
+
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, fled alive the trials at wave 3"
+
        player:mortem_print(" in the Hell Arena...")
+
    end
+
end
+
 
+
function arena.run()
+
Level.name = "Hell Arena" --that IS what it's called
+
        Leve.name_number = 0      --remove floor #
+
Level.fill("prwall")      --ultimately fills border with indestructible red walls
+
local translation = {
+
['.'] = "floor",  --gray floor
+
[','] = "blood",  --red floor (blood)
+
['#'] = "rwall",  --red wall (bloodstone)
+
['>'] = "stairs"  --gray (normal) stairs
+
}
+
 
+
        --create map cell string (76x18)
+
local map = [[
+
#######################.............................########################
+
###########.....................................................############
+
#####..................................................................#####
+
##........................................................................##
+
#..........................................................................#
+
............................................................................
+
..................................,,,,,,....................................
+
..,,,.............................,,,,,,,...................................
+
..,>,............................,,,,,,,,,..................................
+
..,,,............................,,,,,,,,...................................
+
..................................,,,,,,....................................
+
............................................................................
+
............................................................................
+
#..........................................................................#
+
##........................................................................##
+
#####..................................................................#####
+
###########.....................................................############
+
#######################.............................########################
+
]]
+
        --create pillar cell string (6x5)
+
local column = [[
+
,..,.,
+
,####.
+
.####,
+
.####.
+
,..,.,
+
]]
+
+
        --puts map in non-border area
+
Level.place_tile( translation, map, 2, 2 )
+
        --adds random pillars and blood
+
Level.scatter_put( area.new(5,3,68,15), translation, column, "floor",9+math.random(8))
+
Level.scatter( area.FULL_SHRINKED,"floor","blood",100)
+
        --inserts player
+
Level.player(38,10)
+
end
+
</source>
+
  
A link to the source files will be provided soon. It will include the main .module folder, main.lua, module.lua, and the sound and music subfolders with their necessary files.
+
Source data: [http://dl.dropbox.com/u/54818507/arena.module.zip arena.module]

Latest revision as of 17:09, 8 April 2012

In many ways, learning through example is a solid method of figuring out how to create your own code. The archetypical example we use here is Hell's Arena, which has been the example mod of DoomRL since the inception of the sandbox. It imitates Hell's Arena as it appears in DoomRL itself, which a few additions added here and there to create a different experience.

As with all example modules in these tutorials, we will first begin with any radically-new concepts that require plenty of room, then go on to explain things by declaration. Usually this would involve looking at any cells/items/beings (or just old-fashioned tables) but this module has no new objects. Instead, we will only need to move through each engine hook and how it affects the map itself. As a measure of extra caution, generic comments will be added to this map as a means to provide understanding for those skipping all the extra details.

Contents

Special Concept: Adding Sound and Music

The most significant departure from the Arena's vanilla counterpart is the inclusion of custom sounds and music. This is explained sufficiently in the module documentation, but the steps to doing so will be laid out explicitly here.

  • First and foremost, you need to the sound and/or music files. Sounds must be a .wav file, and music must be a .mid, .mp3, or .ogg file. (Note that using .mp3 or .ogg can easily make WAD files much bigger files, so take caution when adding them to your module.) It is suggested that you name your sound and music files something that describes the audio of the file, as its filename (minus the extension) will be used verbatim in the lua script.
  • Second, we need to put them into the module correctly. This is done by creating additional subdirectories in your .module folder:
    • sound files go into "sound"
    • music files go into "music"
  • Whenever you want to access one of these sound or music files, you will use one of the following methods:
    • Sounds are most commonly called using being:play_sound(filename), where "being" is whomever you want the sound to originate from. For "surround" sounds, use player:play_sound().
    • If you want the sound at a very specific point on the map, use Level.explosion() and set the radius, delay, and damage of the explosion to zero: this removes any physical part of the explosion, allowing you to play only a sound at that point.
    • Music is added using core.play_music(filename). It will override any music already playing. (Unfortunately there is no way to turn off the music: if you want no music to play, create a music file that has no audio and use it instead.)
  • Do not modify sound.lua or music.lua. This is technically a way to modify (or include) sounds and music for particular objects/calls, but neither of these files are included in the source folder or WAD file. For the sake of players wanting an easy dump into their DoomRL folder, it is higly recommended that you stick to the aforementioned procedure instead.

In the Hell's Arena module, Skulltag sounds are added at the start of the map, as well as the start of each wave. Music by Simon Volpert, named "Rounds of Hell", is also included at the beginning of the map and changes for each wave. (Adding sounds to beings is a lot more complicated, and we won't get into it here. Suffice it to say that it may prove more trouble than it's worth, not to mention that default sounds cannot be overwritten.)

Engine Hooks

.run()

--always declare the module_id global
core.declare( "arena" , {} )
 
function arena.run()
    Level.name = "Hell Arena"                    --that IS what it's called
    Level.name_number = 0                        --remove floor #
    Level.fill("rwall")                         --immortal border tiles
    local translation = {
        ['.'] = "floor",                         --gray floor
        [','] = "blood",                         --red floor (blood)
        ['#'] = "rwall",                         --red wall (bloodstone)
        ['>'] = "stairs"                         --gray (normal) stairs
	}
 
     --create map cell string (76x18)
	local map = [[
#######################.............................########################
###########.....................................................############
#####..................................................................#####
##........................................................................##
#..........................................................................#
............................................................................
..................................,,,,,,....................................
..,,,.............................,,,,,,,...................................
..,>,............................,,,,,,,,,..................................
..,,,............................,,,,,,,,...................................
..................................,,,,,,....................................
............................................................................
............................................................................
#..........................................................................#
##........................................................................##
#####..................................................................#####
###########.....................................................############
#######################.............................########################
	]]
    --create pillar cell string (6x5)
    local column = [[
,..,.,
,####.
.####,
.####.
,..,.,
	]]
 
    --puts map in non-border area
    Level.place_tile( translation, map, 2, 2 )
    --adds random pillars and blood
    Level.scatter_put( area.new(5,3,68,15), translation, column, "floor",9+math.random(8))
    Level.scatter( area.FULL_SHRINKED,"floor","blood",100)
    --all walls become indestructible
    Generator.set_permanence(area.FULL)
    --inserts player
    Level.player(38,10)
end

If you recall in the tutorial Constructing a Map, we used the starting coordinate (1,1) for the Level.place_tile() method, rather than (2,2). This is because we included the map's border tiles in our test map. It is, in fact, possible to customize the border of the map (there are some issues with non-cell objects on the border, so it is suggested you don't allow for them): however, if the border of the map is going to be uniform, then when you use Level.fill(), fill the level with the border tiles you want. Then, when you write out the map string, you can ignore their existence altogether (the dimensions of such a map string should be 76x18), and place the map string at the starting coordinate (2,2) instead.

Additionally, we have another cell string on this map: a 6x5 group consisting of a pillar surrounded by floor tiles. Hell's Arena randomly scatters these tiles around using the Level.scatter_put() method. The arguments are as follows:

  • Area in which the cell string can be placed
  • character/map translation table (thus you should write this cell string in the same terms as your map's)
  • name of the cell string
  • cell_id on which this cell string can be placed (the method will be sure that the entire cell string can replace only the cell_id you input here)
  • number of times cell string is placed

In the example, a fairly large area is carved out for these pillars to be placed, defining the same translation as for the normal map, using the variable name of the cell string, replacing the "floor" cell, and will be added anywhere from 10-17 times. Note that the middle of the map uses "blood" floor tiles rather than "floor", so that pillar will never appear here.

Finally, after placing the pillars, blood floor files are randomly added to the floor using the Level.scatter() method. Level.scatter() is similar to Level.scatter_put() but has no translation/map arguments. Instead, it asks for the area affected, the cell_id that will replace, the cell_id being replaced, and the number of times it should be added. In the example, "floor" is replaced with "blood" 100 times on any non-border tile (area.FULL_SHRINKED is equivalent to area.new(2,2,77,19)).

.OnEnter()

function arena.OnEnter()
    core.play_music("rounds_of_hell_part_1")         --use some kick-ass music
    player:play_sound("preparetofight")               --skulltag announcer!
    player.eq.weapon = item.new( "shotgun" )          --give the man a shotty
 
    --add 50 shotgun shells to inventory in a single "shell" item
    local shells = item.new( "shell" )
    shells.ammo = 50
    player.inv:add( shells )
    ....

The OnEnter() hook begins in a way you'd tend to expect: special sound/music is added and the player's inventory is adjusted (specifically they are given a shotgun in-hand and fifty shells in the bank).

    ....
    Level.result(1)                                   --sets up first wave
 
    --big announcement stuff
    ui.msg("A devilish voice announces:")
    ui.msg("\"Welcome to Hell's Arena, mortal!\"")
    ui.msg("\"You are either very foolish, or very brave. Either way I like it!\"")
    ui.msg("\"And so do the crowds!\"")
    player:play_sound("fight")
    ui.msg("Suddenly you hear screams everywhere! \"Blood! Blood! BLOOD!\"")
    ui.msg("The voice booms again, \"Kill all enemies and I shall reward thee!\"")
    player:play_sound("one")
    ....

The next part of OnEnter() is a series of ui.msg() methods to display text onto the player's top part of the screen. Sounds are added are relevant points of the monologue. Note that, although ui.msg_enter() isn't being used, the player will have to press ENTER once two lines of text is fully displayed. Getting the timing of this with sounds can be tricky, so I would personally recommend that you use ui.msg_enter() just before a sound is to be played.

Also included here is the Level.result() method. Level.result() can be used in two ways:

  • Calling without any arguments returns the current value of Level.result()
  • Calling with an integer argument sets Level.result() to that integer value

This value can be used for a number of things, but it is most often associated with events occurring in the level.

    ....
    --create the first wave
    Level.summon("demon",3)                           --3 demons
    Level.summon("lostsoul",2)                        --2 lost souls
    Level.summon("cacodemon",DIFFICULTY-1)            --0/1/2/3/4 cacodemons
end

Finally, OnEnter() spawns us some enemies using the Level.summon() method. This is a very simple function: input the being_id you want to spawn and how many, and the game will randomly generate them anywhere on the map. To spawn enemies in a specific area, you will want to use Level.area_summon(), which includes an area argument.

The number of cacodemons in the level depends on the difficulty, which can be called through a pre-defined global variable called DIFFICULTY. On I'm Too Young To Die, DIFFICULTY is equal to 1; on Hey, Not Too Rough, DIFFICULTY is equal to 2; and so on and so forth.

.OnKill()

function arena.OnKill()
        --randomized cheers from the crowd
	local temp = math.random(3)
	if     temp == 1 then ui.msg("The crowds go wild! \"BLOOD! BLOOD!\"") 
	elseif temp == 2 then ui.msg("The crowds cheer! \"Blood! Blood!\"") 
	else                  ui.msg("The crowds cheer! \"Kill! Kill!\"") end
end

The OnKill() hook is used in Hell's Arena to add some crowd cheering. It is randomized between three messages using a temporary variable that changes every time another enemy dies (as the variable is re-defined each time the hook is triggered).

.OnKillAll()

function arena.OnKillAll()
    if Level.result() == 1 then                       --if first wave completes
        --more talk
        ui.msg("The voice booms, \"Not bad mortal! For a weakling that you ")
        ui.msg("are, you show some determination.\"");
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
        ui.msg("The voice continues, \"I can now let you go free, or")
        ui.msg("you may try to complete the challenge!\"");
 
        --player can choose to continue
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
        if choice then                                --player chose to continue
            core.play_music("rounds_of_hell_part_2")  --second verse better than first
            player:play_sound("two")
            ui.msg("The voice booms, \"I like it! Let the show go on!\"")
            ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
 
            Level.drop("chaingun")                    --sweet drop
            Level.summon("demon",3)                   --3 demons
            Level.summon("cacodemon",DIFFICULTY)      --1/2/3/4/5 cacodemons
        --player chose to stop
        else
            ui.msg("The voice booms, \"Coward!\" ")
            ui.msg("You hear screams everywhere! \"Coward! Coward! COWARD!\"")
        end
    ....

OnKillAll() contains the bulk of code in this map, since each time a wave of enemies is killed, a number of things change. The first condition required is to check which wave we're on using Level.result(). Recall that Level.result() is equal to 1 based on the OnEnter() hook, so the first OnKillAll() will trigger for this first case.

After another lengthy explanation, the player is given a y/n confirmation using the ui.msg_confirm() method. This returns a true/false value depending on the input of the player: in the example we assign this return value to choice. Thus we can use this variable to determine what happens next:

  • In the event that the player chooses to accept with the "y" key (choice is set to true), then we get another music change, some displayed praise, and some items and beings added to the level.
  • In the event that the player choosen to decline with the "n" key (choice is set to false), then we get a displayed insult.

Level.drop() should be self-explanatory: is it the item equivalent of Level.summon(). Both the Level.drop() and Level.summon() methods can have the number of items/beings omitted: if so, the default value of 1 is used.

    ....
    elseif Level.result() == 2 then                   --if second wave completes
        --even more talk
        ui.msg("The voice booms, \"Impressive mortal! Your determination")
        ui.msg("to survive makes me excited!\"")
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
        ui.msg("\"I can let you go now, and give you a small reward, or")
        ui.msg("you can choose to fight the final challenge!\"")
 
	--player can choose to continue
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
        if choice then                           --player chosen to continue
            core.play_music("rounds_of_hell_part_3")  --strongest music yet
            player:play_sound("three")
            ui.msg("The voice booms, \"Excellent! May the fight begin!!!\"")
            ui.msg("You hear screams everywhere! \"Kill, Kill, KILL!\"")
 
            --some more sweet loot
            Level.drop("shell",4)
            Level.drop("ammo",4)
            --spawns are a little more difficulty-dependent
            if DIFFICULTY <= 2 then              --ITYTD and HNTR
                Level.summon("cacodemon",DIFFICULTY+1)
            elseif DIFFICULTY == 3 then          --HMP
                Level.summon("knight",2)
            elseif DIFFICULTY >= 4 then          --UV and N!
                Level.summon("baron",2)
            end
        else                                     --player chose to stop
            ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" ")
            ui.msg("You hear screams everywhere! \"Boooo...\"")
 
           --pointless loot
           Level.drop("shell",3) 
           Level.drop("lmed")
           Level.drop("smed")
        end
    ....

This part of the OnKillAll() code shows for the case of Level.result() equal to 2. It is roughly the same as the first case (different music/items/summons/messages aside), but with the following nontrivial changes:

  • The enemies used in the final wave are included in an if-then-elseif conditional statement, with each condition depending on DIFFICULTY. We have three settings: cacodemons (2 on ITYTD, 3 on HNTR), hell knights (on HMP), and barons of hell (on UV and N!).
  • If the player chooses to decline, they will still receive some items. This is simply how it works in the vanilla map, and serves no real purpose in a single-level module such as this.
    ....
    elseif Level.result() == 3 then              --if third wave completes
        --finally done talking
        ui.msg("The voice booms, \"Congratulations mortal! A pity you came to")
        ui.msg("destroy us, for you would make a formidable hell warrior!\"")
        ui.msg("\"I grant you the title of Hell's Arena Champion!\"")
        ui.msg("\"And a promise is a promise... search the arena again...\"")
 
        --also pointless loot
        Level.drop("scglobe")
        Level.drop("barmor")
        Level.drop("lmed")
    end
    ....

Finally, OnKillAll() checks for the case of Level.result equal to 3: this is after all three waves have been killed. The announcer finishes entirely and the player receives some superfluous rewards.

    ....
    --increment level counter each time a wave completes
    Level.result(Level.result()+1)
end

This final piece of code is very important: each time OnKillAll() is triggered, after all the Level.result() checks are made, Level.result() is incremented by one. This means that, after the first wave of enemies is killed, the case of Level.result equal to 1 is executed, the other cases are ignored, and then Level.result() is set to 2. This repeats for the other cases, albeit with different lines being executed before Level.result() is incremented.

.OnExit()

function arena.OnExit()
    ui.msg_enter("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
 
    --takes care of mortem text for "killed by something unknown..."
    if Level.result() < 4
        then arena.result = "fled alive the trials at wave "..Level.result()
        else arena.result = "completed the trials"
    end
end

Once the player leaves the level (and OnExit() triggers), a final statement is given (which must be confirmed to continue, lest it be missed entirely) and a new field to arena is added, this time a string called result. If Level.result() is less than 4 (which is to say, all waves were not killed) then arena.result writes out when the player left using Level.result() as the wave number. If Level.result() is 4 (or technically greater) then arena.result writes out that the player finished. But where is this string going to?

.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, fled alive the trials at wave 3"
        player:mortem_print(" in the Hell Arena...")
end

The OnMortem() hook is called whenever the player has exited the level, immediately after OnExit(), and can be used to change what is displayed on the mortem screen. Here we see arena.result in action:

  • If the player is killed by an enemy, that being's kill_desc (or kill_desc_melee) can be called using player.killedby, which is then set to the local variable kill. If not, kill will be empty when the first line executes.
  • If arena.result was created (and will thus be true), then kill is instead set to that string. Since arena.result is only created if the player chooses to exit the game, this will not overwrite the case where the player is actually killed. It is done mostly to avoid any instances in which there would be a "killed by something unknown..." message.

Finally, the first two lines of the mortem are replaced with the player:mortem_print() method. player.name is the name of the player (as determined in-game); player.explevel is the character level; and klasses[player.klass].name checks the class prototype and returns the player's chosen class's name (for example, "Marine").

Review

Here is the entire main.lua file to view at once:

--always declare the module_id global
core.declare( "arena" , {} )
 
function arena.OnEnter()
    core.play_music("rounds_of_hell_part_1")          --use some kick-ass music
    player:play_sound("preparetofight")               --skulltag announcer!
    player.eq.weapon = item.new( "shotgun" )          --give the man a shotty
 
    --add 50 shotgun shells to inventory in a single "shell" item
    local shells = item.new( "shell" )
    shells.ammo = 50
    player.inv:add( shells )
 
    Level.result(1)                                   --sets up first wave
 
    --big announcement stuff
    ui.msg("A devilish voice announces:")
    ui.msg("\"Welcome to Hell's Arena, mortal!\"")
    ui.msg("\"You are either very foolish, or very brave. Either way I like it!\"")
    ui.msg("\"And so do the crowds!\"")
    player:play_sound("fight")
    ui.msg("Suddenly you hear screams everywhere! \"Blood! Blood! BLOOD!\"")
    ui.msg("The voice booms again, \"Kill all enemies and I shall reward thee!\"")
    player:play_sound("one")
 
    --create the first wave
    Level.summon("demon",3)                           --3 demons
    Level.summon("lostsoul",2)                        --2 lost souls
    Level.summon("cacodemon",DIFFICULTY-1)            --0/1/2/3/4 cacodemons
end
 
function arena.OnKill()
        --randomized cheers from the crowd
    local temp = math.random(3)
    if     temp == 1 then ui.msg("The crowds go wild! \"BLOOD! BLOOD!\"") 
    elseif temp == 2 then ui.msg("The crowds cheer! \"Blood! Blood!\"") 
    else                  ui.msg("The crowds cheer! \"Kill! Kill!\"") end
end
 
function arena.OnKillAll()
    if Level.result() == 1 then                       --if first wave completes
        --more talk
        ui.msg("The voice booms, \"Not bad mortal! For a weakling that you ")
        ui.msg("are, you show some determination.\"");
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
        ui.msg("The voice continues, \"I can now let you go free, or")
        ui.msg("you may try to complete the challenge!\"");
 
        --player can choose to continue
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
        if choice then                                --player chose to continue
            core.play_music("rounds_of_hell_part_2")  --second verse better than first
            player:play_sound("two")
            ui.msg("The voice booms, \"I like it! Let the show go on!\"")
            ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
 
            Level.drop("chaingun")                    --sweet drop
            Level.summon("demon",3)                   --3 demons
            Level.summon("cacodemon",DIFFICULTY)      --1/2/3/4/5 cacodemons
        --player chose to stop
        else
            ui.msg("The voice booms, \"Coward!\" ")
            ui.msg("You hear screams everywhere! \"Coward! Coward! COWARD!\"")
        end
 
    elseif Level.result() == 2 then                   --if second wave completes
        --even more talk
        ui.msg("The voice booms, \"Impressive mortal! Your determination")
        ui.msg("to survive makes me excited!\"")
        ui.msg("You hear screams everywhere! \"More Blood! More BLOOD!\"")
        ui.msg("\"I can let you go now, and give you a small reward, or")
        ui.msg("you can choose to fight the final challenge!\"")
 
    --player can choose to continue
        local choice = ui.msg_confirm("\"Do you want to continue the fight?\"")
        if choice then                                --player chosen to continue
            core.play_music("rounds_of_hell_part_3")  --strongest music yet
            player:play_sound("three")
            ui.msg("The voice booms, \"Excellent! May the fight begin!!!\"")
            ui.msg("You hear screams everywhere! \"Kill, Kill, KILL!\"")
 
            --some more sweet loot
            Level.drop("shell",4)
            Level.drop("ammo",4)
            --spawns are a little more difficulty-dependent
            if DIFFICULTY <= 2 then              --ITYTD and HNTR
                Level.summon("cacodemon",DIFFICULTY+1)
            elseif DIFFICULTY == 3 then          --HMP
                Level.summon("knight",2)
            elseif DIFFICULTY >= 4 then          --UV and N!
                Level.summon("baron",2)
            end
        else                                     --player chose to stop
            ui.msg("The voice booms, \"Too bad, you won't make it far then...!\" ")
            ui.msg("You hear screams everywhere! \"Boooo...\"")
 
           --pointless loot
           Level.drop("shell",3) 
           Level.drop("lmed")
           Level.drop("smed")
        end
 
    elseif Level.result() == 3 then              --if third wave completes
        --finally done talking
        ui.msg("The voice booms, \"Congratulations mortal! A pity you came to")
        ui.msg("destroy us, for you would make a formidable hell warrior!\"")
        ui.msg("\"I grant you the title of Hell's Arena Champion!\"")
        ui.msg("\"And a promise is a promise... search the arena again...\"")
 
        --also pointless loot
        Level.drop("scglobe")
        Level.drop("barmor")
        Level.drop("lmed")
    end
 
    --increment level counter each time a wave completes
    Level.result(Level.result()+1)
end
 
function arena.OnExit()
    ui.msg_enter("The voice laughs, \"Flee mortal, flee! There's no hiding in hell!\"")
 
    --takes care of mortem text for "killed by something unknown..."
    if Level.result() < 4
        then arena.result = "fled alive the trials at wave "..Level.result()
        else arena.result = "completed the trials"
    end
end
 
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, fled alive the trials at wave 3"
        player:mortem_print(" in the Hell Arena...")
end
 
function arena.run()
    Level.name = "Hell Arena"                    --that IS what it's called
    Level.name_number = 0                        --remove floor #
    Level.fill("rwall")                         --immortal border tiles
    local translation = {
        ['.'] = "floor",                         --gray floor
        [','] = "blood",                         --red floor (blood)
        ['#'] = "rwall",                         --red wall (bloodstone)
        ['>'] = "stairs"                         --gray (normal) stairs
    }
 
     --create map cell string (76x18)
    local map = [[
#######################.............................########################
###########.....................................................############
#####..................................................................#####
##........................................................................##
#..........................................................................#
............................................................................
..................................,,,,,,....................................
..,,,.............................,,,,,,,...................................
..,>,............................,,,,,,,,,..................................
..,,,............................,,,,,,,,...................................
..................................,,,,,,....................................
............................................................................
............................................................................
#..........................................................................#
##........................................................................##
#####..................................................................#####
###########.....................................................############
#######################.............................########################
]]
    --create pillar cell string (6x5)
    local column = [[
,..,.,
,####.
.####,
.####.
,..,.,
]]
 
    --puts map in non-border area
    Level.place_tile( translation, map, 2, 2 )
    --adds random pillars and blood
    Level.scatter_put( area.new(5,3,68,15), translation, column, "floor",9+math.random(8))
    Level.scatter( area.FULL_SHRINKED,"floor","blood",100)
    --all walls become indestructible
    Generator.set_permanence(area.FULL)
    --inserts player
    Level.player(38,10)
end

Source data: arena.module