Skip to content

A mission

A mission for one or two players, started from a marker on Grove Street: escort a dealer to a meeting, everyone together, then fight off the Ballas who show up, before the time runs out. It uses most of the mission helper: a start marker, a mission ped to protect, a waiting point, a section with catch-up, a timer that fails it, a counter, and a reward with a cooldown.

mission.ts
import { Server, Weapon, defineMission, type Ped } from "@sagl/server";

const GROVE = { x: 2491, y: -1668, z: 13.3 };
const MEETING = { x: 2337, y: -1748, z: 13.5 };
const BALLAS_AT = [
  { x: 2320, y: -1760, z: 13.5 },
  { x: 2352, y: -1765, z: 13.5 },
  { x: 2330, y: -1730, z: 13.5 },
];

const theMeeting = defineMission({
  name: "The Meeting",
  maxPlayers: 2,
  cooldown: 10 * 60_000,   // once every ten minutes per player
  minDuration: 30_000,     // nobody walks it in under 30 s
  requires: (p) => (p.wantedLevel > 0 ? "Lose the cops first." : undefined),

  async run(m) {
    // The dealer: a mission ped, seen by the party, who keeps to his orders.
    const dealer = m.createPed(28, { x: 2495, y: -1672, z: 13.3 }, 90, { mission: true, weapon: Weapon.Pistol });
    m.blip(dealer, { color: "blue" });
    m.failWhen(dealer, "died", "The dealer died.");

    m.objective("Walk the ~b~dealer~w~ to the ~y~meeting~w~.");
    void dealer.follow(m.leader, { distance: 3 });
    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 });      // a waiting point: the whole party
    close.stop();

    // Anyone who wandered off is brought here before the fight.
    await m.section("fight", { at: [MEETING, { ...MEETING, x: MEETING.x + 2 }] });
    await m.step(dealer.walkTo({ ...MEETING, y: MEETING.y + 4 }));   // into the doorway
    void dealer.cower();
    m.bigText("AMBUSH!", 2000);

    const ballas: Ped[] = BALLAS_AT.map((at) =>
      m.createPed(102, at, 0, { mission: { hostileTo: [...m.players] }, weapon: Weapon.Pistol }));
    for (const b of ballas) m.blip(b, { color: "red" });
    const left = m.counter("Ballas", { value: ballas.length });
    m.timer("Time", 90_000, { fail: "The dealer got away." });
    m.objective("Kill the ~r~Ballas~w~.");

    await m.until(() => {
      left.value = ballas.filter((b) => b.exists && !b.dead).length;
      return left.value === 0;
    });
    m.pass({ money: 1500 });
  },
});

export function missions(server: Server) {
  server.missions.marker(theMeeting, GROVE);
  server.on("missionEvent", (e) => console.log(`[mission] ${e.type} ${e.name} ${e.player?.name ?? ""} ${e.reason}`));
}

Things to notice:

  • Waits stop when the mission ends. If the dealer dies on the way, m.reach rejects with MissionEnded and the script stops there; the helper catches it, removes the peds, blips and HUD, and shows "MISSION FAILED!" with the reason.
  • keepClose tells a player who runs ahead to wait for the dealer (only them), and fails the mission if the whole party leaves him behind.
  • Ped orders never reject, so m.step(dealer.walkTo(...)) wraps one to make it stop with the mission too.
  • Late joiners (m.addPlayer(friend)) get the objective, blips, counter and timer as they are, and see the mission's peds.
  • The reward is paid once, by pass(), to each player in the party.