Countdown (LUA)

Overview

In this Lua mapadd example, a Combine terminal is used to trigger a download sequence and the workstation must be defended against Civil Protection officers determined to intercept the transfer and lock the player out of the system.

Newcomers to SMOD's Lua system are advised to first read through the Supply Drop article where the script is broken down better, often line-for-line. This article skimps on detailing the basic syntax in order to cut down on its considerable length.

Due to the complexity involved and need to keep its length (somewhat) reasonable, this article is more a dissection of an example script than a walkthrough of the process like most of the TXT mapadd articles on the wiki. Try to carefully read through and think about how the methods described could be applied to your own mapadd ideas. More hands-on readers should check the download link at the bottom.

The reader should already be familiar with mapadd syntax, grabbing coordinates, using the input/output system, and other basics. If a term seems unfamiliar, try the site glossary.

It can be helpful while going through this page to run SMOD in a window so you can quickly and easily switch back and forth.
You can do this by right-clicking on it in your Steam games menu, choosing Properties, choosing Set Launch Options, and adding -window.

How-To

Node Check

Begin by picking a map you'd like to work with and ensure it has a matching SNL file in your mapadd\nodes folder. The method used to grab random locations depends on having an SNL file and crashes are possible without one. AINs will not suffice so if you don't have an SNL, try generating one.

Prepping the TXT

Before diving into the Lua, open up your map and use the Physics Launcher and Physics Gun to spawn and position a downloading prop, using physlaunch_model to pick an appropriate worldmodel. Pasting the snippet below in the console will start you off with these tools and a simple Combine console for a model.

give item_suit;give weapon_physgun;give weapon_physlauncher;physlaunch_class simple_physics_prop;physlaunch_model models/props_combine/breenconsole.mdl

Once you have your prop ready to go, use the Physics Launcher to absorb it and print its coordinates to the console.

countphysgun_t.jpg countphyslauncher_t.jpg countplcoords_t.png

Start an Entities section in the mapadd's TXT and add the block from the console. Afterward, change it to a static prop_physics_override and give it a targetname like so:

"Entities"
    {
    "prop_physics_override"
     {
     "origin" "6442.4 6281.3 384.6"  "angle" "0 -93 360"
     "keyvalues"
     {
     "model" "models\props_combine\breenconsole.mdl"
     "targetname" "terminal"
     "spawnflags" "264" // Motion disabled; generate output upon +USE
     }
     }

Below that, add a relay and point_clientcommand so sv_cheats is enabled. This is so the cheat-protected command instant_trig_run can be used to run a mapadd label instead of using an instant_trig workaround. Note that simply using sv_cheats 1 is by default blocked by the ignore_point_command variable, so the incrementvar command is used instead.

"point_clientcommand"
     {
     "keyvalues"
     { "targetname" "cmd" }
     }
    "logic_relay"
     {
     "keyvalues"
     {
     "spawnflags" "1" // Terminate this relay once done.
     "OnSpawn" "cmd,Command,incrementvar sv_cheats 1 1 1,0,-1"
     // Needed for "instant_trig_run" command. Using bullet-time toggles this anyway.
     "OnSpawn" "cmd,Command,give item_suit,0,-1"
     "OnSpawn" "cmd,Command,give weapon_ar2,0,-1"
     "OnSpawn" "cmd,Command,give item_ammo_ar2_large,0,-1"
     }
     }

After that comes a call to the Lua script's Init function with the Lua command, followed by a new label for calling the DownTimerInit function, which will be used to create the download clock.

"lua"
     {
     "callfunc" "Init"
     }
    }
"Entities:Call_DownTimerInit"
    {
    "lua"
     {
     "callfunc" "DownTimerInit"
     }
    }

Last but not least, a steady stream of Civil Protection officers should be attacking during the transfer, so three npc_maker ents like that below in the Entities section should do the trick (at least for this demonstration):

"npc_maker"
     {
     "origin" "5317 5388 64" "angle" "0 64 0"
     "keyvalues"
     {
     "targetname" "metro_maker"
     "spawnflags" "160" // Infinite children, do not spawn while visible.
     "StartDisabled" "1"
     "NPCType" "npc_metropolice"
     "NPCSquadName" "metro_squad"
     "NPCTargetname" "metroA1"
     "additionalequipment" "weapon_shotgun"
     "MaxLiveChildren" "1"
     "SpawnFrequency" "-1" // Spawn a new officer as soon as the current one is dead.
     "OnSpawnNPC" "down_schedule,StartSchedule,,1,-1"
     }
     }

Three important things to note here:

  • The makers are disabled at start, to be enabled through their targetnames later.
  • The targetname of their spawned NPCs will be metroX where "X" is A1, A2, and A3 for each maker. Each officer will need a distinct targetname for their individual presence at the terminal to trigger its shutdown. If they all had the exact same name, all three would need to stand closely to the terminal. Since NPCs will sidestep each other to avoid collision, this is unrealistic.
  • The first maker in the set will call down_schedule each time it spawns an officer (OnSpawnNPC), to ensure that newly spawned officers continue to push toward the terminal rather than focus strictly on the player once he begins injuring them.

Also included in the example are two small labels, Down_Success and Down_Failure which simply trigger some text and an env_fade, depending on if the player succeeded in transferring the data or not. With the TXT side set up, we can get into the Lua side of things.

Vars and Init

After creating/opening up a map's Lua script, writing some variables at the top can help you get a better idea of what you want from the script and how it should be achieved. Try to make variable names simple to remember. Below is an example snippet where the main variables are specified in caps to better differ between them and temporary ones the different functions will use.

DOWN_TIME = 40
-- Base download time in seconds.
DOWN_TIMESHIFT = { -8,20 }
-- A random number between the two values in this array will be added to DOWN_TIME for the final time.
DOWN_TIMEc = { nil }
-- An array for holding download percentages. Used later in DownTimeFormat and DownTimerCount.
-- Ex. With a final DOWN_TIME of 40, this array will go "2.5, 5.0, 7.5, 10.0" and so on 'til it reaches 100.

After that, a small "Init" function that calls DownTimeFormat and then DownInit will give the TXT side something to run. It's nice to have a function like this for starting all your other scripts, as it helps keep things tidy and easier to read.

function Init()
    DownTimeFormat()
    DownInit()
end

DownInit

This is a relatively tiny function that simply searches for the terminal added in the TXT side, grabs its origin coordinates, then hands both off to DownPrep to process.

1
2
3
4
5
6
7
8
9
function DownInit()
    local terminal = HL2.FindEntityByName(nil, "terminal")
    if terminal ~= nil then
     local org = HL2.GetEntityAbsOrigin(terminal)
     DownPrep(terminal, org)
    else
     print("ERROR: DownInit - Could not find 'terminal'.")
    end
end

DownPrep

This function's task is to add a keyvalue to the terminal so the player can trigger the sequence, then add some needed entities to pull the whole thing off. For this article, it's broken up into three parts: DownPrep, DownPrepAud, and DownPrepCP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function DownPrep(terminal, org)
    print("DEBUG: DownPrep - Executing.")
    HL2.KeyValue(terminal, "OnPlayerUse", "down_starter,Trigger,,0,1")

    local start = HL2.CreateEntity("logic_relay", VECTORZERO, VECTORZERO)
    if start ~= nil then
     HL2.KeyValue(start, "targetname", "down_starter")
     HL2.KeyValue(start, "spawnflags", "1")
     HL2.KeyValue(start, "OnTrigger", "down_finisher,Trigger,,"..DOWN_TIME..",-1")
     HL2.KeyValue(start, "OnTrigger", "down_startaud,PlaySound,,0,-1")
     HL2.KeyValue(start, "OnTrigger", "down_workaud,ToggleSound,,1,-1")
     HL2.KeyValue(start, "OnTrigger", "metro_maker,Enable,,0,-1")
     HL2.KeyValue(start, "OnTrigger", "cmd,command,instant_trig_run Call_DownTimerInit,0,-1")
     HL2.SpawnEntity(start)
    end
    local finish = HL2.CreateEntity("logic_relay", VECTORZERO, VECTORZERO)
    if finish ~= nil then
     HL2.KeyValue(finish, "targetname", "down_finisher")
     HL2.KeyValue(finish, "spawnflags", "1")
     HL2.KeyValue(finish, "OnTrigger", "down_breaker,Kill,,0,-1")
     HL2.KeyValue(finish, "OnTrigger", "down_workaud,ToggleSound,,0,-1")
     HL2.KeyValue(finish, "OnTrigger", "down_doneaud,PlaySound,,0.1,-1")
     HL2.KeyValue(finish, "OnTrigger", "metro_maker,Disable,,0,-1")
     HL2.KeyValue(finish, "OnTrigger", "cmd,command,instant_trig_run Down_Success,0,-1")
     HL2.SpawnEntity(finish)
    end
    local breaker = HL2.CreateEntity("logic_relay", VECTORZERO, VECTORZERO)
    if breaker ~= nil then
     HL2.KeyValue(breaker, "targetname", "down_breaker")
     HL2.KeyValue(breaker, "spawnflags", "1")
     HL2.KeyValue(breaker, "OnTrigger", "down_finisher,Kill,,0,-1")
     HL2.KeyValue(breaker, "OnTrigger", "down_workaud,ToggleSound,,0,-1")
     HL2.KeyValue(breaker, "OnTrigger", "down_breakaud,PlaySound,,0,-1")
     HL2.KeyValue(breaker, "OnTrigger", "metro_maker,Disable,,0,-1")
     HL2.KeyValue(breaker, "OnTrigger", "cmd,command,instant_trig_run Down_Failure,0,-1")
     HL2.SpawnEntity(breaker)
    end
    DownPrepAud(org)
    DownPrepCP(org)
end

Line 3: Adds an OnPlayerUse output to the terminal prop that will trigger down_starter below.
Lines 5-15: Creates and spawns a relay, down_starter. This relay is set to

  • Trigger down_finisher once the countdown ends (if it hasn't been deleted by down_breaker).
  • Play a starting sound and toggle a working sound, which will play 'til the transfer's end.
  • Enable the metro_maker ents outside the building to start spawning troops.
  • Run the Call_DownTimerInit label, which in turn starts the Lua script's DownTimerInit function.

Lines 16-26: Creates and spawns another relay, down_finisher. This is rigged to

  • Delete down_breaker and prevent the officers from stopping the completed download.
  • Play a finishing sound and toggle the working sound off.
  • Disable the metro_maker ents from spawning more troops.
  • Trigger the Down_Success label in the TXT side.

Lines 27-37: Creates and spawns one last relay, down_breaker, which can be triggered by the officers if one reaches the terminal. Its outputs are scripted to

  • Delete down_finisher and prevent the download from finishing in the player's favor.
  • Play an alternative finishing sound and toggle the working sound off.
  • Disable the metro_maker ents from spawning more troops.
  • Trigger the Down_Failure label in the TXT side.

Lines 38-39: Calls the other DownPrep functions, passing on the terminal's origin coordinates.

DownPrepAud

Nothing to really explain here; this function merely creates a group of sounds used by the terminal throughout the script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function DownPrepAud(org)
    print("DEBUG: DownPrepAud - Executing.")
    local doneaud = HL2.CreateEntity("ambient_generic", org, VECTORZERO)
    if doneaud ~= nil then
     HL2.KeyValue(doneaud, "targetname", "down_doneaud")
     HL2.KeyValue(doneaud, "spawnflags", 1 + 16 + 32) -- Play everywhere, start silent, do not loop.
     HL2.KeyValue(doneaud, "message", "buttons/combine_button3.wav")
     HL2.KeyValue(doneaud, "health", "8")
     HL2.SpawnEntity(doneaud)
    end
    local startaud = HL2.CreateEntity("ambient_generic", org, VECTORZERO)
    if startaud ~= nil then
     HL2.KeyValue(startaud, "targetname", "down_startaud")
     HL2.KeyValue(startaud, "spawnflags", 16 + 32) -- Start silent, do not loop.
     HL2.KeyValue(startaud, "message", "buttons/combine_button1.wav")
     HL2.KeyValue(startaud, "health", "8")
     HL2.KeyValue(startaud, "radius", "500")
     HL2.SpawnEntity(startaud)
    end
    local workaud = HL2.CreateEntity("ambient_generic", org, VECTORZERO)
    if workaud ~= nil then
     HL2.KeyValue(workaud, "targetname", "down_workaud")
     HL2.KeyValue(workaud, "spawnflags", "16") -- Start silent.
     HL2.KeyValue(workaud, "message", "ambient/levels/labs/equipment_beep_loop1.wav")
     HL2.KeyValue(workaud, "health", "4")
     HL2.KeyValue(workaud, "radius", "500")
     HL2.SpawnEntity(workaud)
    end
    local breakaud = HL2.CreateEntity("ambient_generic", org, VECTORZERO)
    if breakaud ~= nil then
     HL2.KeyValue(breakaud, "targetname", "down_breakaud")
     HL2.KeyValue(breakaud, "spawnflags", 16 + 32) -- Start silent, do not loop.
     HL2.KeyValue(breakaud, "message", "buttons/combine_button2.wav")
     HL2.KeyValue(breakaud, "health", "4")
     HL2.KeyValue(breakaud, "radius", "500")
     HL2.SpawnEntity(breakaud)
    end
    local idleaud = HL2.CreateEntity("ambient_generic", org, VECTORZERO)
    if idleaud ~= nil then
     HL2.KeyValue(idleaud, "spawnflags", "0")
     HL2.KeyValue(idleaud, "message", "ambient/machines/combine_terminal_loop1.wav")
     HL2.KeyValue(idleaud, "health", "4")
     HL2.KeyValue(idleaud, "radius", "500")
     HL2.SpawnEntity(idleaud)
    end
end

DownPrepCP

This function is a bit more interesting than the last, spawning entities the Civil Protection officers will interact with.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function DownPrepCP(org)
    print("DEBUG: DownPrepCP - Executing.")
    local lx = 1
    local ly = 3
    for lx=1,ly,1 do
     local trig = HL2.CreateEntity("instant_trig", org, VECTORZERO)
     if trig ~= nil then
     HL2.KeyValue(trig, "targetname", "down_trig")
     HL2.KeyValue(trig, "radius", "64")
     HL2.KeyValue(trig, "touchname", "metroA"..lx)
     HL2.KeyValue(trig, "group", "1")
     HL2.KeyValue(trig, "removegroup", "1")
     HL2.KeyValue(trig, "OnHitTrigger", "down_breaker,Trigger,,0,-1")
     HL2.SpawnEntity(trig)
     end
    end
    local schedule = HL2.CreateEntity("aiscripted_schedule", VECTORZERO, VECTORZERO)
    if schedule ~= nil then
     HL2.KeyValue(schedule, "targetname", "down_schedule")
     HL2.KeyValue(schedule, "spawnflags", "4") -- repeatable
     HL2.KeyValue(schedule, "m_iszentity", "metroA*")
     HL2.KeyValue(schedule, "goalent", "terminal")
     HL2.KeyValue(schedule, "graball", "1") -- Use all entities with this targetname at once.
     HL2.KeyValue(schedule, "forcestate", "2") -- Force them into an alert stance.
     HL2.KeyValue(schedule, "schedule", "2") -- Run to the goal entity, the terminal.
     HL2.KeyValue(schedule, "interruptability", "1") -- Unless you've been injured (or killed), don't stop for anything.
     HL2.SpawnEntity(schedule)
    end
end

Lines 3-16: Starts a for loop that will create a proximity trigger for each spawned officer's targetname (metroA1, metroA2…), near the terminal. Once tripped, it will trigger down_breaker, then delete itself and any other down_trig ents (group 1, removegroup 1).

Lines 17-28: Spawns a repeatable aiscripted_schedule which will keep the officers focused on reaching the terminal instead of just fighting the player. The schedule will be reapplied each time that the first metro_maker ent spawns a replacement soldier.

DownTimeFormat

Now come the more complicated tasks of chopping up DOWN_TIME for use in a download status message ("2.5%", "5%"), formatting it appropriately, and then showing the player that message. First up is DownTimeFormat, called by Init shortly before DownPrep.

1
2
3
4
5
6
7
8
9
10
11
12
13
function DownTimeFormat()
    print("DEBUG: DownTimeFormat - Executing.")
    local b = DOWN_TIME
    DOWN_TIME = DOWN_TIME + HL2.RandomInt(DOWN_TIMESHIFT[1],DOWN_TIMESHIFT[2])
    print("DEBUG: DownTimeFormat - DOWN_TIME changed, "..b.."s to "..DOWN_TIME.."s.")
    local timebasechop = 1/DOWN_TIME

    local x = 1
    local y = DOWN_TIME
    for x=1,y,1 do
     DOWN_TIMEc[(y-x+1)] = DownTimeRound(timebasechop*x*100)
    end
end

Line 3: Stores DOWN_TIME's current value in a temporary variable, "b", to help track what comes into the function compared to what comes out.

Line 4: Adds a random integer between DOWN_TIMESHIFT's two values to DOWN_TIME.

Line 5: Makes a note in the console on how DOWN_TIME's been tweaked, using the "b" variable.

Line 6: Calculates DOWN_TIME's reciprocal. With the example time of 40, this comes out to "0.025". Stores the resulting value in a temporary variable, "timebasechop".

Line 8-12: Begin a for loop that starts at 1 and keeps going in increments of 1 until it's run through [DOWN_TIME] loops.

Line 11: timebasechop's value is multiplied by the loop's current x value, then multiplied by 100 and if necessary, rounded by DownTimeRound. This value is then stored inside the DOWN_TIMEc array, using "y-x+1" to determine the slot. "y-x+1" is used to reverse the order DOWN_TIMEc's slots are used, as the countdown timer later will be displaying the value of DOWN_TIMEc[DOWN_TIME], then subtracting from DOWN_TIME.

An example of DOWN_TIME: 40 and x: 1-4 would therefore go:

DOWN_TIMEc[y-x+1] = DownTimeRound(timebasechop*x*100)
DOWN_TIMEc[40-1+1] = DownTimeRound(0.025 * 1 * 100)
DOWN_TIMEc[40] = DownTimeRound(2.5)
DOWN_TIMEc[40-2+1] = DownTimeRound(0.025 * 2 * 100)
DOWN_TIMEc[39] = DownTimeRound(5.0)
DOWN_TIMEc[38] = DownTimeRound(7.5)
DOWN_TIMEc[37] = DownTimeRound(10.0)
And so on up to 100.0.

DownTimeRound

This function's small task, as part of DownTimeFormat's process, is to take any unruly decimal numbers and cut them down to size. For instance, if DownTimeFormat was given a DOWN_TIME of 45, the very first calculation would come up with "2.2222222222222222222222222222222", which would be a (fairly comical) distraction for the player to see pop up in their HUD message. This function will cut the fractional part down to the last two rounded digits beyond the decimal separator, "2.22" in this case, and return the value to DownTimeFormat.

1
2
3
4
5
6
7
function DownTimeRound(num)
    local digits = 2
    local shift = 10 ^ digits
    local result = math.floor( num*shift + 0.5 ) / shift
    print("DEBUG: DownTimeRound: In: "..num..", Out: "..result..".")
    return result
end

Line 2: Creates the "digits" variable, for painlessly changing how many fractional digits are left in as desired.

Line 3: Declares the "shift" variable, its value calculated from 10digits. This will be used to convert the desired digits into an integer for rounding.

Line 4: Calculates the value of num (the value handed down from DownTimeFormat) multiplied by shift and added to by 0.5. The math.floor function is used to round the result off to an integer smaller or equal to it. The result is then divided by shift and stored in the "result" variable.

The daunting example above of num: 2.22… would go:

result = math.floor(num * shift + 0.5) / shift
result = math.floor(2.2222222222222222222222222222222 * 100 + 0.5) / 100
result = math.floor(222.22222222222222222222222222222 + 0.5) / 100
result = math.floor(222.72222222222222222222222222222) / 100
result = 222 / 100
result = 2.22

If you haven't guessed yet, the need for "+ 0.5" in this equation is due to the way math.floor works, which is by always rounding down. Here, adding 0.5 tweaks its behavior by forcing it to round up numbers that warrant this. For example, here you can see how 5.5876 is rounded up to 5.59:

result = math.floor(num * shift + 0.5) / shift
result = math.floor(5.5876 * 100 + 0.5) / 100
result = math.floor(558.76 + 0.5) / 100
result = math.floor(559.26) / 100
result = 559 / 100
result = 5.59

DownTimerInit

Almost there! This function is called by the down_starter relay, triggered by the player's use of the terminal. Its purpose is simply to start up the timer function (via HL2.CreateTimer), DownTimerCount, which will repeat every 1 second.

1
2
3
4
5
function DownTimerInit()
    print("DEBUG: DownTimerInit - Executing.")
    HL2.CreateTimer("DownTimerCount", 1)
    return 0
end

DownTimerCount

The very last function, and what much of the script has been building up to. After being called by DownTimerInit, this will start a countdown to the player's successful download, though it can be interrupted by a CP officer making his way to the terminal.

1
2
3
4
5
6
7
8
9
10
11
function DownTimerCount()
    local ent = HL2.FindEntityByName(nil, "down_trig")
    local timestr = "\127 "..DOWN_TIMEc[DOWN_TIME].."%"
    HL2.ShowInfoMessage(0,1,timestr)

    DOWN_TIME = DOWN_TIME - 1
    if DOWN_TIME == 0 or ent == nil then
     return 0
    end
    return 1
end

Line 2: Declares "ent", specified as the first entity found targetnamed down_trig.

Line 3: Declares "timestr", a string concatenating "\127 " (the SMOD font's stopwatch icon, which replaces the usual tilde, followed by a space), DOWN_TIMEc[DOWN_TIME] (using decimals stored from DownTimeFormat), and a percentage sign. The first time this function ran with a DOWN_TIME of 40, this would come out as "[watch] 2.5%".

Line 4: Uses HL2.ShowInfoMessage to relay the above string to the player's HUD, with green text that will display for 1 second, like so:

countclockshot.png

Line 6: Decrements DOWN_TIME by 1 to keep the countdown moving.

Lines 7-9: If DOWN_TIME is equal to 0 or ent is nil (meaning the down_trig group has been triggered), return 0 to terminate the timer.

Line 10: If DOWN_TIME is not equal to 0 and the down_trig group is still present, continue with the countdown.

Result and Wrap-Up

With the above or a similar set-up built, you should now be able to activate the terminal to start the download and send Civil Protection rushing your way. Treat yourself to a cookie if you managed to wade through all the above, eh?

counttimeround_t.png countterminal_t.jpg countfight01_t.jpg countfight02_t.jpg

The purpose of this page, along with the other mapadd pages, is to serve as a starting place for your own mapadd ideas. Bear in mind that just about anything can be done, it's just a matter of figuring how to make it happen.

mapaddex_dl.png The example script(s) this article is based on can be downloaded here for reference along with SNL data. Simply extract to your mapadd folder and open up dm_overwatch once in-game.


Customization

Mapadd Command Reference (Non-LUA)Command Reference (LUA)Getting StartedPorts 'n' Doors
Alarm, Alarm!Color Correction in SMODDoor BreachingMobile APCsWorking With Dropships
Supply Drop (LUA)Countdown (LUA)
kh0rn3's Mapadd Generator
Scripts addcontentsoverride_classsmod_custom_explosivesmodaclistSMOD Soundscripts

npc_gib_modelnpc_replace_modelnpc_shieldsetnpc_weapon_randomizenpc_weaponweight

excludeweaponsweapon_categoryweapon_customConsole Command List
Other Crosshair CustomizationGenerating AI NodesUsing the NodemakerSubViewCam
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License