Scripting
Missions are written in Lua, using threads, waits and events.
The shape of a script
Section titled “The shape of a script”mission.lua runs when the mission loads. It sets things up and registers sections, then stops. The mission itself runs inside the sections.
mission.api(1)
mission.section("landing", { start = "spawn" }, function() objective.add("regroup", "Find your squad", { waypoint = "shed" }) trigger.wait("shed_area") objective.complete("regroup") mission.gotoSection("barn")end)
mission.section("barn", { start = "shed" }, function() sayAndWait("spencer", "Good to see you.") mission.complete()end)Every mission.lua must call mission.api(1) at the top and register at least one section.
Sections
Section titled “Sections”A section is one stretch of the mission, and the first one registered runs first. opts.start names the marker the squad begins it at.
Sections also make testing quick, because you can jump straight to any of them. See Debugging.
Threads
Section titled “Threads”A thread runs alongside the rest of the script, and waiting in one doesn’t hold up anything else.
thread(function() while true do wait(30) say("spencer", "Keep moving.") endend)delay(seconds, fn) runs a function later on its own thread.
Events
Section titled “Events”notify raises an event and waittill waits for one. An event has a key and a name.
thread(function() waittill("farm", "alarm") music.set("combat")end)
-- elsewherenotify("farm", "alarm")The game raises events too:
| Key | Event | Raised when |
|---|---|---|
actor:<id> |
interact |
A player interacts with the actor |
actor:<id> |
death |
The actor dies |
stealth |
detected |
An enemy spots the squad |
zone |
left, returned |
A player crosses the mission area’s edge |
document |
collected |
A player picks up a document |
wave:<name> |
spawned |
A man in a wave spawns |
sequence:<name> |
finished |
A sequence finishes |
charge:<id> |
planted, detonated |
A charge is planted or goes off |
endon(key, event) ends the calling thread when an event fires. runUntil(key, event, fn) runs a function that an event can cut short, such as dialogue interrupted by an alarm:
if not level.alarm and runUntil("farm", "alarm", briefing) then objective.complete("listen")endrunUntil only sees events that fire after it starts, so check the state first, as above.
A flag is a value that stays set, so a thread that checks late doesn’t miss it.
flag.set("bridge_blown")flag.wait("bridge_blown")Sharing state
Section titled “Sharing state”Keep state that must last across sections in a global table named level:
level = { farmAlerted = false,}The top of the script runs again on a restart. Plain data in level is saved at each checkpoint and put back when the player resumes, but health, ammo and picked-up items are not saved. Hand out kit again at the top of each section.
Splitting a script up
Section titled “Splitting a script up”include runs another Lua file from the mission folder. Its locals stay private, so share through globals or its return value.
local village = include("village.lua")Text and translations
Section titled “Text and translations”Text can be a plain string or a translation:
say("spencer", tr("my_campaign.spencer.hello", character("hollis")))tr takes a key from your pack’s lang files, and further arguments fill %1$s, %2$s in the line. character(id) passes a character’s display name.
Limits
Section titled “Limits”Scripts run in a sandbox. There’s no file or system access, and load and loadstring are removed. A thread that runs too long without waiting is stopped, so give long loops a wait.
