Lua HUD Manual
by Andrew Apted. October 2008OVERVIEW
Lua is a scripting language, which is quite easy to learn even if you've never programmed before. More information can be found here: Lua home pageEDGE looks for several lumps to load Lua scripts from. Lumps called "LUAUTIL#" (where # is a digit) contain utility code, useful stuff that is not HUD-related, whereas lumps called "LUAHUD#" contain the code for drawing the HUD (which is nearly everything on the screen while the user is playing a level).
The following lumps are looked for (in the order listed) and are loaded when present. Each lump is only loaded once, so when two pwads have a "LUAHUD1" lump (for example), then only the second one is loaded.
LUMP | Source |
LUAUTIL0 | from EDGE.WAD |
LUAUTIL1 | from a TC/Project |
LUAUTIL2 | from a Mod/Addon |
LUAHUD0 | from EDGE.WAD |
LUAHUD1 | from a TC/Project |
LUAHUD2 | from a Mod/Addon |
The lumps ending with '0' are the ones in EDGE.WAD and should not be replaced by custom pwads (especially LUAUTIL0 which has some important house-keeping functions). Lumps ending with '1' are for large projects and Total Conversions, which generally have new maps, textures and monsters. Lumps ending with '2' are for smaller mods/addons, stuff like new weapons, which could be used in addition to a large project or TC.
LUMP CONTENTS
The contents of each lump is simply the text of the Lua code.The engine provides two modules: the "hud" module provides drawing functions and general queries, whereas the "player" module provides query functions about the current player. All of their functions and variables are described in separate sections below.
In order to customise the default HUDs, your Lua code needs to redefine one of the existing functions, as follows:
doom_status_bar() : replace this function if you only want to customise the full status bar (including the one shown in the automap screen). The size has to be the same (width 320, height 32).
overlay_status_bar() : write your own version of this function if you only want to change the overlay status bar.
doom_automap() : this function draws the automap screen (including the status bar at the bottom). Replacing it means you can show other information here instead of (or in addition to) the automap.
hud.draw_all() : this is the function which EDGE calls to draw everything. The normal version (in EDGE.WAD) will call the above functions depending on the user's current HUD and whether the automap is active or not. Replacing this function gives you total control: you could provide more huds (or less) than the usual three, ignore the automap mode completely if you wanted, or even draw the view from multiple players.
HUD MODULE
General Queries
hud.automap
This variable is true while the user is viewing the automap
(by pressing TAB) and false for the normal view.
hud.which
This variable is the current HUD number which the user cycles
through when pressing '+' and '-' keys. It ranges from 0 to 119,
allowing 120 different HUD screens, but in reality you must
use the modulo operator '%' to convert this number to a smaller
range. For a single HUD, simply ignore this value. For two HUDs
use 'hud.which % 2'. For three HUDs use 'hud.which % 3',
and so forth... The following are good modulo numbers:
2,3,4,5,6,8 and 10 (because they divide into 120).
hud.now_time
This variable contains the current time, in terms of "tics"
where there are 35 tics per second. In other words, after each
1/35th of a second the value of hud.now_time increases by one.
It keeps going even during menus or while the game is paused.
hud.passed_time
This variable contains the number of "tics" that have passed
since the last time the hud.draw_all() function was called.
Note that a result of zero is possible.
hud.game_mode()
This function returns a string for the current game mode:
"sp" (Single Player),
"coop" (Cooperative), or
"dm" (Deathmatch).
hud.game_name()
This function returns the DDF name of the current game being
played (the one defined in GAMES.DDF).
hud.map_name()
This function returns the DDF name of the current map being
played (the one defined in LEVELS.DDF).
hud.map_title()
This function returns the title of the map being played,
mainly to be displayed on the automap.
Drawing Stuff
hud.coord_sys(w, h)
In the original DOOM, the screen size was always 320x200,
and by default all of the drawing functions here use screen
coordinates as if that were the case (even when EDGE is running
in different modes likes 640x480 or 1024x768).
This function allows you to set a different "virtual" resolution,
for example 640x400, and then all coordinates will be for this
new system, plus the size of images and text characters will be
affected as well.
hud.text_font(name)
Sets the current text font, where the 'name' parameter refers to an
entry in FONTS.DDF.
The default font is "DOOM" and is reset after each frame.
hud.text_color(name)
Sets the current text color, which must refer to an entry in
COLMAPS.DDF, or can be the empty string
"" which causes the text to be
drawn normally (without being colormapped).
The default is "" and is reset after each frame.
hud.set_scale(value)
Sets the scaling for drawing text and for hud.draw_image().
Larger values make the text/image bigger.
The default scale is 1.0 and is reset after each frame.
hud.set_alpha(value)
Set the alpha value (translucency) for drawing text, lines, boxes and images.
The 'value' parameter ranges from 0.0 (completely invisible) to 1.0
(completely solid).
The default alpha is 1.0 and is reset after each frame.
hud.solid_box(x, y, w, h, color)
Draws a solid rectangle consisting of a single color.
The 'x' and 'y' parameters are the coordinates of the top left
corner, whereas 'w' and 'h' are the width and height.
The current alpha value is also applied.
The 'color' parameter can take two different forms. Firstly it may be a string with the same notation as DDF and HTML, which begins with a "#" character and is followed by 6 hexadecimal digits. For example "#FF0000" for red and "#0000FF" for blue. Secondly it can be a Lua table with fields called 'r', 'g' and 'b' (for red, green and blue). Each of these fields is a number from 0 to 255. For example: { r=255, g=170, b=0 } for orange.
hud.solid_line(x1, y1, x2, y2, color)
Draws a solid line between the start coordinate (x1,y1) to the
end coordinate (x2, y2).
The 'color' parameter is the same as for hud.solid_box(), and
the current alpha value is also applied.
hud.thin_box(x, y, w, h, color)
Similar to hud.solid_box(), but only draws the outline of a rectangle.
The inside area is not affected. The sides are always two pixels
thick, and never go outside the specified area.
The 'color' parameter is the same as for hud.solid_box(), and
the current alpha value is also applied.
hud.gradient_box(x, y, w, h, TL, BL, TR, BR)
Similar to hud.solid_box(), but the colors for each corner are
specified individually: 'TL' for top left, 'BL' for bottom left,
'TR' for top right and 'BR' for bottom right.
The current alpha value will also be applied.
hud.draw_image(x, y, name)
Draws an image at the given coordinates, which specify the top/left
corner of the image. The current alpha and scaling factors are
applied as well.
hud.stretch_image(x, y, w, h, name)
Similar to hud.draw_image(), but the image will be stretched or squashed
so that it fits exactly into the given rectangle on the screen.
The current alpha value is also applied.
hud.tile_image(x, y, w, h, name, [x_offset, y_offset])
Draws an image (usually a texture or flat) on the screen,
where the image is tiled (repeated) to fill up the given rectangle.
The current alpha and scaling factors are also applied.
The 'x_offset' and 'y_offset' parameters are optional,
and can be used to offset the texture by a certain number of pixels.
hud.draw_text(x, y, str)
Draws some text on the screen using the current text font, color,
alpha and scaling values. Newlines ("\n")
in the string can be used to draw multi-line text.
hud.draw_num2(x, y, len, num)
Draws a number (an integer) on the screen using the current text font, color,
alpha and scaling. The number is right-aligned, in other words
the 'x' parameter specified the right-most pixel, and the 'len'
parameter gives the maximum number of characters (including the minus
sign if the number is negative).
hud.render_world(x, y, w, h)
Renders the view for the player on the screen, in a
rectangle with the given coordinates. The player's weapon is
also drawn. The views of different players can be rendered
by using the hud.set_render_who() function below.
hud.render_automap(x, y, w, h, [options])
Renders the automap for the player on the screen, in a
rectangle with the given coordinates. Note that no background is
drawn, hence you can use this function to create an overlay automap
(drawn over the top of the player's view). If you need a solid color
behind it, use the hud.solid_box() function first.
The 'options' parameter is optional, when present it is a table containing a set of variables which modify the way the automap is drawn. Variables which are not present in the table are not affected (stay the same as the user's normal automap). The following list shows all the possible variables:
Variable | Description |
zoom | Set a fixed zoom factor, where 1.0 shows the whole map, and larger values make the map bigger |
grid | force the grid lines on/off |
rotate | force map rotation on/off |
follow | force follow-player mode on/off |
things | draw all things |
walls | draw all walls (like IDDT cheat) |
allmap | draw walls like All-Map powerup |
hud.set_render_who(index)
Sets the current player for rendering the world or the
automap. The 'index' parameter is a
small number: 1 for the "main player" on this computer
(the person at the keyboard), 2 for the next player in the list, etc...
upto the number of players in the game.
hud.automap_colors(table)
This function can be used to change some or all of the colors used
when drawing the automap. The 'table' parameter is a Lua table
where the names are automap parts and the values are the colors.
For example: { grid = "#006666", wall = "#FFFFFF" }.
Parts that are not present in the table are not affected (stay the same as before).
Here is a list of all the automap parts that can be changed:
Automap Part | Description |
grid | Grid lines |
wall | One sided walls |
step | Floor height change, climable |
ledge | Floor drop-off, too high to climb |
ceil | Ceiling height difference |
secret | Secret doors |
allmap | Unseen walls when you have the All-Map |
player | Player object |
monster | Monsters |
corpse | Dead monsters |
item | Pickup items |
missile | Missiles, fireballs, etc |
scenery | Scenery items |
Audio Functions
hud.play_sound(name)
Plays the given sound, which must be an entry in SOUNDS.DDF.
PLAYER MODULE
General Queries
player.is_bot()
Returns true if the current player is a bot.
player.get_name()
Returns the name of the current player.
player.health()
Returns the health of the current player. The result will normally
be in the range 0 to 100, regardless of the SPAWNHEALTH setting for
the player in DDF (in other words, the result is a percentage value
of the spawn health). Values higher than 100 are possible when the
player has bonus health (e.g. from the Soul Sphere pickup)
player.armor(type)
For the given armor type, returns the amount the player is
currently wearing. The 'type' parameter is a number in the range 1-5,
but the following names can be used for more readable code:
ARMORS.green |
ARMORS.blue |
ARMORS.purple |
ARMORS.yellow |
ARMORS.red |
player.total_armor(type)
Returns the total amount of armor the player has.
player.frags()
Number of frags the player has (for Deathmatch games).
player.move_speed()
Returns a number for how fast the player is currently moving,
roughly the number of map units per tic (there are 35 tics
per second).
player.air_in_lungs()
Returns amount of air in the player lungs, as a percentage
value from 0 to 100.
Only guaranteed to be valid while the player is underwater.
player.has_key(key)
Returns true if the player currently has the specified key,
which is a number from 1 to 16. For more readable code, the
following names can be used:
KEYS.blue_card | KEYS.gold_key |
KEYS.red_card | KEYS.brass_key |
KEYS.yellow_card | KEYS.steel_key |
KEYS.green_card | KEYS.fire_key |
KEYS.blue_skull | KEYS.silver_key |
KEYS.red_skull | KEYS.copper_key |
KEYS.yellow_skull | KEYS.wooden_key |
KEYS.green_skull | KEYS.water_key |
player.has_power(power)
Returns true if the player currently has the specified powerup.
The 'power' parameter is a number from 1 to 16.
For more readable code, the following names can be used:
POWERS.invuln |
POWERS.berserk |
POWERS.invis |
POWERS.acid_suit |
POWERS.automap |
POWERS.goggles |
POWERS.jet_pack |
POWERS.night_vis |
POWERS.scuba |
player.power_left(power)
Returns the number of seconds remaining for the specified powerup,
or zero when the player does not have it.
The berserk powerup only counts down the red-screen effect, and returns -1
when that is finished.
The automap powerup returns a large value when active and it never
counts down.
The result for invulnerability is not affected by the God-mode cheat.
Weapon Stuff
player.has_weapon(name)
Returns true if the player currently owns the weapon, where 'name'
is the DDF name of the weapon.
player.has_weapon_slot(slot)
Returns true if the player currently owns any weapon which uses
the given 'slot', which is a number for 0 to 9 (same as the
BINDKEY command in the DDF).
player.cur_weapon()
Returns the DDF name of the weapon the player is currently
holding, or the special value "none"
when the player is holding no weapon at all, or
"change" while the weapon is
switching to a new one.
player.cur_weapon_slot()
Returns the slot number (i.e. BINDKEY) of the weapon the
player is currently holding, or -1 when the player is holding no
weapon at all.
player.ammo(type)
Returns the amount of ammo the player is carrying (not including
any ammo inside the clips of weapons).
The 'type' parameter is a number in the range 1-16.
For more readable code, one of the following names can be
used instead:
AMMOS.bullets | AMMOS.pellets |
AMMOS.shells | AMMOS.nails |
AMMOS.rockets | AMMOS.grenades |
AMMOS.cells | AMMOS.gas |
player.ammomax(type)
Returns the maximum amount of ammo the player can carry
(not including weapon clips). The 'type' parameter is the
same as the player.ammo() function.
player.main_ammo()
Returns the main ammo quantity for the player's current weapon.
This is zero for weapons that don't use any ammo (like the FIST).
If the weapon has a clip and the SHOWCLIP command (in DDF) is
true, then the amount of ammo inside the clip is returned instead.
Note that only the primary attack is checked, the secondary attack
(if present) will be ignored.
player.ammo_type(ATK)
Returns the ammo type of the player's current weapon for the
given attack (primary or secondary). The result is in the
range 1-16, or can be 0 for the special case of NOAMMO.
The 'ATK' parameter is 1 for the primary attack, 2 for the
secondary attack, and is compulsory.
player.ammo_pershot(ATK)
Returns the ammo used up per shot by the current weapon for the
given attack (primary or secondary). Same as the AMMOPERSHOT
commands in WEAPONS.DDF.
The 'ATK' parameter is 1 for the primary attack, 2 for the
secondary attack, and is compulsory.
player.clip_ammo(ATK)
Returns the current amount of ammo the clip in the player's current
weapon is holding, or zero if the weapon has no clip.
The 'ATK' parameter is 1 for the primary attack, 2 for the
secondary attack, and is compulsory.
player.clip_size(ATK)
Returns the maximum amount of ammo the clip in the player's current
weapon can hold, or zero if the weapon has no clip.
The 'ATK' parameter is 1 for the primary attack, 2 for the
secondary attack, and is compulsory.
player.clip_is_shared()
Returns true if the player's current weapon is sharing a single
clip between primary and secondary attackes (the SHARED_CLIP
command).
Conditions
player.on_ground()
Returns true if player is standing on solid ground.
player.under_water()
Returns true if player is in AIRLESS water and doesn't have the
Scuba powerup.
player.is_swimming()
Returns true if player is in swimmable water (i.e. the SWIM sector special).
player.is_jumping()
Returns true if player is jumping.
player.is_crouching()
Returns true if player is crouching.
player.is_attacking()
Returns true if player is firing his weapon (either first or second attack).
player.is_rampaging()
Returns true if player has been firing his weapon for two seconds or more.
player.is_using()
Returns true if player is holding the USE button down.
player.is_grinning()
Returns true if player is grinning (after picking up a weapon).
Miscellaneous
player.num_players()
Returns the total number of players in the game, including bots.
player.set_who(index)
Sets who the current player is. The 'index' parameter is a
small number: 1 for the "main player" on this computer
(the person at the keyboard), 2 for the next player in the list, etc...
upto the number of players in the game.
All the player query functions described here return their results
for the current player.
player.hurt_by()
If the player has been hurt in the last few seconds, this
returns a string describing what did the damage. Otherwise
this function returns nil.
The result is usually "enemy",
but could be "friend" for
friendly fire. If the player hurt himself with his own damn stupidity then the
result is "self",
whereas damaging floors and crushers will return
"other".
player.hurt_mon()
If the player has been hurt in the last few seconds, this
returns the name of the monster or other player. Otherwise
this function returns nil.
player.hurt_pain()
If the player was just hurt, this returns the damage amount,
otherwise this function returns 0.
player.hurt_dir()
If the player was just hurt, this returns a direction relative to the
player where the attacker was: -1 for the left side, +1 for the right
side, and 0 for all other cases.
player.hurt_angle()
Like player.hurt_dir(), except this returns the map angle from the
player to his attacker. The result is in degrees (ranging from
0 to 359), where East is 0 and North is 90.
STANDARD HUD CODE
Here is what standard HUD code stored in EDGE.WAD looks like (with syntax highlighting). It implements the original DOOM status bar, including all the logic for the Doomguy face, and other HUD elements (e.g. the underwater AIR bar).
-------------------------------------------- -- HUD LUA CODE for EDGE -- Copyright (c) 2008 The Edge Team. -- Under the GNU General Public License. -------------------------------------------- face_time = 0 face_image = "STFST01" function doom_weapon_icon(slot, x, y, off_pic, on_pic) if player.has_weapon_slot(slot) then hud.draw_image(x, y, on_pic) else hud.draw_image(x, y, off_pic) end end function doom_key(x, y, card, skull, card_pic, skull_pic, both_pic) local has_sk = player.has_key(skull) if player.has_key(card) then hud.draw_image(x, y, sel(has_sk, both_pic, card_pic)) elseif has_sk then hud.draw_image(x, y, skull_pic) end end function doomguy_face(x, y) -- This routine handles the face states and their timing. -- The precedence of expressions is: -- -- dead > evil grin > turned head > straight ahead local function pain_digit() local health = math.min(100, player.health()) local index = int(4.99 * (100 - health) / 100); assert(index >= 0) assert(index <= 4) return tostring(index) end local function turn_digit() return tostring(int(math.random() * 2.99)) end local function select_new_face() -- dead ? if player.health() <= 0 then face_image = "STFDEAD0" face_time = 10 return end -- evil grin when player just picked up a weapon if player.is_grinning() then face_image = "STFEVL" .. pain_digit() face_time = 7 return end -- being attacked ? if player.hurt_by() then if player.hurt_pain() > 50 then face_image = "STFOUCH" .. pain_digit() face_time = 35 return end local dir = 0 if player.hurt_by() == "enemy" then dir = player.hurt_dir() end if dir < 0 then face_image = "STFTL" .. pain_digit() .. "0" elseif dir > 0 then face_image = "STFTR" .. pain_digit() .. "0" else face_image = "STFKILL" .. pain_digit() end face_time = 35 return end -- rampaging? if player.is_rampaging() then face_image = "STFKILL" .. pain_digit() face_time = 7 return end -- god mode? if player.has_power(POWERS.invuln) then face_image = "STFGOD0" face_time = 7 return end -- default: look about the place... face_image = "STFST" .. pain_digit() .. turn_digit() face_time = 17 end ---| doomguy_face |--- face_time = face_time - hud.passed_time if face_time <= 0 then select_new_face() end -- FIXME faceback hud.draw_image(x-1, y-1, face_image) end function doom_status_bar() hud.draw_image( 0, 168, "STBAR"); hud.draw_image( 90, 171, "STTPRCNT"); hud.draw_image(221, 171, "STTPRCNT"); hud.text_font("BIG_DIGIT") hud.draw_num2( 44, 171, 3, player.main_ammo(1)); hud.draw_num2( 90, 171, 3, player.health()); hud.draw_num2(221, 171, 3, player.total_armor()) if hud.game_mode() == "dm" then hud.draw_num2(138, 171, 2, player.frags()); else hud.draw_image(104, 168, "STARMS"); doom_weapon_icon(2, 111, 172, "STGNUM2", "STYSNUM2"); doom_weapon_icon(3, 123, 172, "STGNUM3", "STYSNUM3"); doom_weapon_icon(4, 135, 172, "STGNUM4", "STYSNUM4"); doom_weapon_icon(5, 111, 182, "STGNUM5", "STYSNUM5"); doom_weapon_icon(6, 123, 182, "STGNUM6", "STYSNUM6"); doom_weapon_icon(7, 135, 182, "STGNUM7", "STYSNUM7"); end doomguy_face(144, 169) doom_key(239, 171, 1, 5, "STKEYS0", "STKEYS3", "STKEYS6") doom_key(239, 181, 2, 6, "STKEYS1", "STKEYS4", "STKEYS7") doom_key(239, 191, 3, 7, "STKEYS2", "STKEYS5", "STKEYS8") hud.text_font("YELLOW_DIGIT") hud.draw_num2(288, 173, 3, player.ammo(1)); hud.draw_num2(288, 179, 3, player.ammo(2)); hud.draw_num2(288, 185, 3, player.ammo(3)); hud.draw_num2(288, 191, 3, player.ammo(4)); hud.draw_num2(314, 173, 3, player.ammomax(1)); hud.draw_num2(314, 179, 3, player.ammomax(2)); hud.draw_num2(314, 185, 3, player.ammomax(3)); hud.draw_num2(314, 191, 3, player.ammomax(4)); end function doom_overlay_status() hud.text_font("BIG_DIGIT") hud.draw_num2(100, 171, 3, player.health()); hud.text_color("TEXT_YELLOW"); hud.draw_num2( 44, 171, 3, player.main_ammo(1)); if player.total_armor() > 100 then hud.text_color("TEXT_BLUE"); else hud.text_color("TEXT_GREEN"); end hud.draw_num2(242, 171, 3, player.total_armor()) doom_key(256, 171, 1, 5, "STKEYS0", "STKEYS3", "STKEYS6") doom_key(256, 181, 2, 6, "STKEYS1", "STKEYS4", "STKEYS7") doom_key(256, 191, 3, 7, "STKEYS2", "STKEYS5", "STKEYS8") hud.text_font("YELLOW_DIGIT") hud.text_color(""); hud.draw_num2(288, 173, 3, player.ammo(1)); hud.draw_num2(288, 179, 3, player.ammo(2)); hud.draw_num2(288, 185, 3, player.ammo(3)); hud.draw_num2(288, 191, 3, player.ammo(4)); hud.draw_num2(314, 173, 3, player.ammomax(1)); hud.draw_num2(314, 179, 3, player.ammomax(2)); hud.draw_num2(314, 185, 3, player.ammomax(3)); hud.draw_num2(314, 191, 3, player.ammomax(4)); end function doom_automap() -- Background is already black, only need to use 'solid_box' -- when we want a different color. -- -- hud.solid_box(0, 0, 320, 200-32, "#505050") hud.render_automap(0, 0, 320, 200-32) doom_status_bar() hud.text_font("DOOM") hud.draw_text(0, 200-32-10, hud.map_title()) end function edge_air_bar() if player.health() <= 0 or not player.under_water() then return end local air = player.air_in_lungs() air = int(1 + 21 * ((100-air) / 100.1)) hud.draw_image(0, 0, string.format("AIRBAR%02d", air)) end function hud.draw_all() hud.coord_sys(640, 400) if hud.automap then doom_automap() return end -- there are three standard HUDs hud.which = hud.which % 3 if hud.which == 0 then hud.render_world(0, 0, 320, 200-32) else hud.render_world(0, 0, 320, 200) end if hud.which == 0 then doom_status_bar() elseif hud.which == 2 then doom_overlay_status() end edge_air_bar() end