Lua HUD Manual

by Andrew Apted.   October 2008

OVERVIEW

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 page

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