Skip to content

Missions

import { defineMission, Mission, MissionEnded, MissionRefused } from "@sagl/server";

A mission is a script for a party of players, written top to bottom as in GTA's own missions: create a ped, set an objective, wait for the party to get somewhere, pass. The helper does the bookkeeping around it.

  • The party shares the mission's peds, blips, objective text, counters and timers, including players who join later.
  • Waits (reach, enter, until, ...) say whether the first player completes the step for everyone, or every player must (a waiting point).
  • Sections set where the party respawns, and bring anyone left far behind to the start of the next part, faded out and back in.
  • Fail conditions end it for everyone, stopping the script where it waits.
  • Cleanup: when it ends, its blips and HUD go and its characters fade out, as in GTA: bodies lie where they fell until the cleanup takes them, and its vehicles stay until nobody is near.
  • Exploit tooling: rewards only through pass(), once; cooldowns; requirements checked at start and on pass; passes that are too fast are refused; every start, pass, fail and refusal is a missionEvent.

A mission lives as long as the gamemode: a server restart loses it, and a player who leaves (or reconnects) leaves it.

const delivery = defineMission({
  name: "Delivery",
  maxPlayers: 2,
  cooldown: 10 * 60_000,
  async run(m) {
    const car = m.createVehicle(482, DEPOT, 90)   // a Burrito;
    m.failWhen(car, "wrecked", "The van was destroyed.");
    m.objective("Get in the ~b~van~w~.");
    m.blip(car, { color: "blue" });
    await m.enter(car, { allPlayers: true });
    m.objective("Drive to the ~y~drop-off~w~.");
    m.timer("Time", 120_000, { fail: "Too late." });
    await m.reach(DROP_OFF, { vehicle: car });
    m.pass({ money: 2500 });
  },
});

server.missions.marker(delivery, { x: 2491, y: -1670, z: 13.3 });

Defining a mission

defineMission(definition)

defineMission(definition: MissionDefinition): MissionDefinition

Returns the definition as it is; it gives the editor the types.

Field Type Description
name string For cooldowns and missionEvent.
maxPlayers number Party size. Default 1.
cooldown number After a pass, the same player can't start it again for this long (ms). Failing doesn't count.
minDuration number A pass sooner than this after the start (ms) is refused, with no reward, and logged.
requires (player) => string \| void Checked for each player when they start or join, and again on pass. Return a reason (shown to them) to refuse.
deathFails boolean One player dying fails it. Default false: they respawn at the section start.
catchUpDistance number How far (m) from a new section's start before a player is brought there. Default 150.
run (m: Mission) => Promise<void> The script. If it returns without pass() or fail(), the mission passes. If it throws, the mission fails (reason "error") and the error is logged.
steps MissionStep[] Or the script as named steps, run in order (see below).
state () => S With steps: the state they share, made fresh for each run.

Steps and checkpoints

A script as named steps reads top to bottom, and each step is a checkpoint: the mission can be started from it, which runs its setup first to put in place what the steps before would have left (a ped in a car, the party somewhere). Each step's name is logged as it starts (m.stage).

const job = defineMission<{ benny: Ped }>({
  name: "Clean Hands",
  state: () => ({}) as { benny: Ped },
  steps: [
    { name: "call 1", run: async (m) => { await phoneCall(m); } },
    {
      name: "grab Benny",
      setup: async (m, s) => { s.benny = m.createPed(46, CAB_STAND, 0); },  // only when starting here
      run: async (m, s) => { await m.reach(s.benny.position, { radius: 4 }); },
    },
  ],
});

server.missions.start(job, player, { from: "grab Benny" });   // testing the second half

Starting missions: server.missions

marker(definition, at, options?)

marker(definition: MissionDefinition, at: Vector3, options?: MarkerOptions): MissionMarker

A start marker: a blip and the game's yellow marker for every player who isn't on a mission. Walking into it on foot starts the mission, with anyone else standing in it joining the party (up to maxPlayers).

Option Type Description
radius number Default 1.5 m.
world number Virtual world. Default 0.
blip BlipOptions | false The blip and marker; false for none. Default: the game's yellow.
onRefused (player, reason) => void Default: the reason as an objective line for 3 s.

Returns a MissionMarker; remove() takes it away.

start(definition, players, options?)

start(definition: MissionDefinition, players: Player | Player[], options?: { from?: string }): Mission

Starts a mission for these players; the first is the leader. from starts a mission made of steps at that step (its setup first).

Throws MissionRefused (with reason) if any of them is on a mission, cooling down, or doesn't meet requires, or there are more than maxPlayers.

server.on("playerEnterVehicle", (player, vehicle) => {
  if (vehicle.model !== 420 || player.seat !== 0) return;   // a taxi, driving
  try {
    server.missions.start(taxiDriver, player);
  } catch (e) {
    if (e instanceof MissionRefused) player.hud.objective(e.reason, 3000);
  }
});

of(player), running

The mission a player is on (or null), and every mission running.


Mission

What run gets, and what start returns.

Member Description
name, definition
players The party, leader first.
leader Whoever started it, or the next in line once they leave.
running False once it has ended.
outcome How it ended (MissionOutcome), or null.
done A promise of the MissionOutcome: resolves when it ends, however it ends.
data Free-form storage for the script.
startedAt Date.now() at the start.

The party

addPlayer(player)

Adds a player (an invitation). They get its HUD as it is and see its peds. Throws MissionRefused as start does, or if it's full.

removePlayer(player)

Takes them out; their HUD for it goes. The last player leaving abandons the mission (no screen is shown). Players leaving the server leave their mission.

Creating things

createPed(model, position, heading?, options?)

As server.createPed, plus onEnd. Seen by the party (players joining later too), unless you give visibleTo. When the mission ends it fades out, or if it's dead its body is released: it lies there until it has been out of view a minute. onEnd: "keep" (or keep: true) keeps it for good; onEnd: "destroy" removes even a body at once.

const ryder = m.createPed("ryder", HOUSE, 0, { mission: true, weapon: Weapon.Pistol });

createVehicle(model, position, heading?, options?)

As server.createVehicle, plus onEnd. When the mission ends it's released: it stays until nobody is near (a wreck, until it has been out of view 30 s). onEnd: "destroy" removes it at once (a cab nobody should find afterwards); onEnd: "keep" (or keep) leaves it for good.

createObject(model, position, rotation?, options?), createPickup(options)

As server.createObject and server.createPickup, seen by the party unless visibleTo says otherwise. Destroyed and removed when the mission ends.

HUD

Each shows for the whole party, and for players who join later.

Method Description
objective(text, ms?) Objective text at the bottom (game codes like ~y~ work).
clearObjective()
bigText(text, ms?) Big text in the middle.
blip(target, options?) A blip (and the game's marker) on a spot, ped, vehicle or player: see PlayerHud.blip. Returns a MissionBlip (set(), remove()).
counter(label, options?) "Crates 2/6": { value, max, bar, color }. Returns a MissionCounter (value, label, remove()).
timer(label, ms, options?) A countdown (countUp to count up). With fail: "reason", the mission fails when it reaches 0. value is the time left.
const crates = m.counter("Crates", { max: 6 });
m.timer("Time", 90_000, { fail: "You ran out of time." });
crates.value++;

Waiting

Every wait rejects with MissionEnded if the mission ends first, which stops the script there; the helper catches it, so let it go.

Steps say who has to do them:

Option
{ anyPlayer: true } The first player does it for everyone (the default).
{ allPlayers: true } A waiting point: every player must.

reach(spot, options?)

reach(spot: Vector3, options?: ReachOptions): Promise<Player>

Waits for the party to reach a spot, shown with a blip and the game's yellow marker to those who haven't. With allPlayers, each counts once they've been there and the rest are waited for. Resolves with the player who completed it (the last one, for allPlayers).

Option Type Description
radius number Default 3 m.
vehicle Vehicle They must be in it, driving or riding.
marker boolean | BlipOptions The blip and marker. Default true.
anyPlayer, allPlayers boolean See above.
const first = await m.reach(ROOFTOP);                  // whoever gets there first
await m.reach(MEETING_POINT, { allPlayers: true });    // everyone, before the cutscene
await m.reach(GARAGE, { vehicle: van });               // in the van

enter(vehicle, options?)

enter(vehicle: Vehicle, options?: { anyPlayer?: boolean; allPlayers?: boolean; driver?: boolean }): Promise<Player>

Waits for the party to get in a vehicle (driver: in its driver's seat). allPlayers waits until they're all in it at once.

untilPlayer(condition, options?)

untilPlayer(condition: (player: Player) => boolean, options?: WaitOptions): Promise<Player>

Waits until condition holds for the first player, or for all at once.

const shooter = await m.untilPlayer((p) => p.weapon === Weapon.Sniper);

until(condition, { timeout? })

Resolves with true once condition() is true (checked four times a second); with timeout (ms), with false if that passes first. Give waits in a scene a limit, so a blocked ped can't leave it stuck.

if (!await m.until(() => benny.vehicle === cab, { timeout: 8000 })) benny.putInVehicle(cab, 2);

allDead(peds)

Resolves once every one of these peds is dead or removed.

wait(ms)

Resolves after ms.

step(promise)

Waits for any promise, such as a ped's order, but rejects with MissionEnded if the mission ends first. With { timeout } (ms), resolves with undefined if that passes first.

await m.step(ryder.walkTo(CAR));
await m.step(benny.walkTo(OUTSIDE, { pace: "run" }), { timeout: 4000 });  // knocked over: on anyway

Scenes and speech

cinematic(shot, scene, options?)

cinematic(shot: CameraShot, scene: (cut) => Promise<void>, options?: { players?: Player[]; freeze?: boolean; holster?: boolean; screenEffects?: "clear" | "keep" }): Promise<void>

A cinematic shot for the party (or players): black bars, the camera on shot, everyone frozen and their weapons put away while scene runs. Then the camera's back behind them, they can move and have their weapons again, however the scene ends (the mission ending too). cut(shot) changes the shot; cut(null) goes back to the first. freeze: false and holster: false leave those alone.

Their screen effects are cleared for the scene and come back as they were after (a snapshot), so a player's own depth of field or grading doesn't spoil the shot; the scene can add its own meanwhile. screenEffects: "keep" leaves them on.

await m.cinematic({ position: car.offset({ right: 3.2, ahead: 1.2, up: 0.9 }), lookAt: { vehicle: car } },
  () => m.say("audio/benny-locked.mp3", { at: benny, subtitle: LINE }));

say(file, options?)

A recorded line (a client file) with its subtitles timed to it, shown to the party; the radio ducks under it. Resolves when it's been said, and stops if the mission ends. Takes server.audio.speak's options: at a ped or vehicle to come from them (muffled from inside it), left out at the party's ears (effects: "phone" for a caller).

await m.say("audio/voice-call2.mp3", { effects: "phone", subtitle: CALL2, prefix: "~y~" });
await m.say("audio/benny-my-car.mp3", { at: benny, subtitle: "My car! That's my car!" });

For as long as it runs

on(event, listener), every(ms, fn)

A server event listener, or a function called every ms, for as long as the mission runs: removed when it ends however it ends (so no try/finally), or earlier with the function each returns.

m.on("playerExitVehicle", (player, vehicle) => { if (vehicle === getaway) alarm(); });
const stop = m.every(1000, () => updateChase());

population(settings)

Changes the population of the leader's world (as world.population) until the mission ends, or the function it returns is called: then the settings it changed are as they were.

const fireBack = m.population({ fire: false });  // no fire engines to the car we torch

holdWhileWanted(message?)

While anyone in the party is wanted, the mission's timers stand still and message (default "Lose the cops.") stays as the objective; objectives set meanwhile show once they're clear.

stage(name), currentStage

Marks where the script is: logged, and a missionEvent of type "stage". Steps do it themselves.

keepClose(target, options?)

keepClose(target: Ped | Vehicle | Player, options?: {
  warnAt?: number; warning?: string; failAt?: number; fail?: string;
}): { stop(): void }

Keeps the party near someone: an escort, a ped to follow, a vehicle.

  • A player further than warnAt (default 25 m) gets warning in place of the objective (only them), until they're back in range, when the objective returns.
  • With failAt, the mission fails (fail) once every player is further than that.

It lasts until the mission ends or you stop() it.

const close = m.keepClose(dealer, {
  warnAt: 25, warning: "You're too far away. Wait for the ~b~dealer~w~ to catch up.",
  failAt: 150, fail: "You lost the dealer.",
});
await m.reach(MEETING, { allPlayers: true });
close.stop();

cooldown(key, ms)

true if key was used in the last ms, else starts it again and returns false. Throttles repeated actions:

if (m.cooldown("throw", 1500)) return;

Cutscenes

cutscene(name, options?)

cutscene(name: string, options?: CutsceneOptions): Promise<CutsceneResult>

Plays one of the game's cutscenes for the whole party together; resolves when everyone's has finished. Skipping is a vote by default. The options and result are server.playCutscene's. If the mission ends meanwhile, the cutscene stops.

await m.cutscene("ryder2a");

Sections

section(name, options)

section(name: string, options: { at: Vector3 | Vector3[]; heading?: number; catchUpDistance?: number }): Promise<void>

Starts a part of the mission:

  • Respawning: a party member who dies (while others are alive) respawns, then is brought to the section's start.
  • Catching up: anyone further than catchUpDistance from it, or in another world, is faded out, moved there, and faded back in. Resolves once they're there.

at is one spot, or one per party member in party order so they don't land on top of each other.

await m.section("vault", { at: [VAULT_1, VAULT_2, VAULT_3, VAULT_4], heading: 90 });

Ending

failWhen(...)

failWhen(ped: Ped, event: "died", reason: string): { stop() }
failWhen(vehicle: Vehicle, event: "wrecked", reason: string): { stop() }
failWhen(condition: () => boolean, reason: string): { stop() }

Fails the mission when the ped dies (or is removed), the vehicle is wrecked (or removed), or condition() becomes true; until stop().

m.failWhen(ryder, "died", "Ryder died.");
m.failWhen(() => Math.hypot(ryder.position.x - m.leader.position.x, ryder.position.y - m.leader.position.y) > 150,
  "You left Ryder behind.");

fail(reason?)

Fails it for everyone: "MISSION FAILED!" with reason under it.

pass(reward?)

pass(reward?: { money?: number; text?: string }): void

Passes it: each player gets money, once, and "MISSION PASSED!" with text under it (default: the money, $2500). Refused (no reward, reason tooFast or requirements) if it's sooner than minDuration or a player no longer meets requires.

The mission also fails:

  • for everyone, when every party member is dead at once ("Everyone died."), or when one dies and deathFails is set;
  • with reason abandoned (no screen) when the party empties.

MissionOutcome

Field Type Description
passed boolean
reason "passed" | "failed" | "abandoned" | "tooFast" | "requirements" | "error"
text string The fail reason, or the pass subtitle.
players Player[] Who was in it at the end.
ms number How long it ran.
const m = server.missions.start(delivery, player);
const { passed, ms } = await m.done;
if (passed) player.send(`Delivered in ${Math.round(ms / 1000)} s.`);