Skip to content

Interactions

import { Interaction, type InteractionUse } from "@sagl/server";

Things players use by walking up to them, as the game's vending machines: every map object of some models (vending machines, payphones, ticket machines) or a spot. On foot, close to one and facing it, a player sees its prompt in the help box (top left); the enter/exit key (F/Enter) uses it, and the server decides what happens.

The game's own vending machines, payphones and so on do nothing in SAGL (their scripts were the game's mission script): add them here.

server.interactions.add(options)

add(options: InteractionOptions): Interaction

For everyone, players joining later too.

Option Type Description
models number[] Every map object of these models (up to 16).
at Vector3 Or one spot.
radius number How close they must be (m), facing it. Default 1.5, at most 3.
prompt string In the help box while they can use it. Game codes work: ~k~~VEHICLE_ENTER_EXIT~ is their key. gxt() text too. Left out: no help box (show your own with onReach).
to Player[] Only these players can see and use it (others never hear of it). Default everyone.

Returns an Interaction:

Member Description
onUse(fn) fn(player, use) when a player uses it. use.model, use.position and use.heading (the way it faces, degrees) say which object (model 0: the spot). The server has checked they're there. Returns it.
onReach(fn) fn(player, use, inReach) when it comes in a player's reach (true) and goes (false): to show a UI of your own instead of the prompt. Returns it.
remove() Takes it away for everyone.

Some of the game's models:

Models
Vending machines: drinks 955, 1209, 1302, 1775
Vending machines: snacks 956, 1776, 1977
Payphones 1216
Ticket machines 1779
// A dollar for a drink, and some health back.
server.interactions.add({ models: [955, 1209, 1302, 1775], prompt: gxt("SLOT_05") })   // "Buy Drink: $1"
  .onUse((player) => {
    if (player.money < 1) return server.audio.playGame(1053, { at: player.position, to: [player] });  // the game's "no"
    player.money -= 1;
    player.health = Math.min(100, player.health + 30);
  });

server.interactions.add({ models: [1216], prompt: "Press ~k~~VEHICLE_ENTER_EXIT~ to make a call" })
  .onUse((player) => player.ui.open("ui/phone.rml"));

Your own prompt or UI

Leave out prompt and show your own when it's in reach, from the server:

const atm = server.interactions.add({ at: { x: 2412, y: -1240, z: 24 } });
const shown = new Map<Player, Drawn>();
atm.onReach((player, use, inReach) => {
  if (inReach) shown.set(player, player.draw.text("~g~ATM~w~ - press ~y~F~w~", 0, -80, { anchor: "bottom", align: "center", outline: 1 }));
  else shown.get(player)?.remove();
});
atm.onUse((player) => player.ui.open("ui/atm.rml"));

or from a client script, which can draw it at once: returning true from sagl.interactions.onReach (see sagl) replaces the prompt for any interaction.

Animations, freezing and sounds

Interactions usually come with an animation, maybe the player held still, and a sound: see player.playAnim(), player.frozen and server.audio. The freeroam example's payphone (examples/freeroam/payphone.ts) rings from where it is, and when answered plays the phone animations with the player frozen:

player.frozen = true;
await player.playAnim("PED", "phone_in", { ms: 1200 });
player.playAnim("PED", "phone_talk", { loop: true });
player.subtitle("~y~Caller:~w~ Watch your back, homie.", 5000);

The game's own machines did all this in their scripts, and the freeroam example's vending machines (examples/freeroam/vending.ts) do it as the game's vending script did: the player slid in front of the machine (by its use.heading; slideTo, not a teleport) and held, its sound from them, its animations, then the health:

const h = (use.heading * Math.PI) / 180;   // 1 m in front of it, 0.2 m right
player.frozen = true;
await player.slideTo({ x: use.position.x + 0.2 * Math.cos(h) + Math.sin(h), y: use.position.y + 0.2 * Math.sin(h) - Math.cos(h), z: player.position.z }, use.heading);
server.audio.playMission(42600, { at: player });            // the machine's clunk (42601: snacks)
await player.playAnim("VENDING", "VEND_Use", { ms: 2600 });
await player.playAnim("VENDING", "VEND_Use_pt2", { ms: 400 });
await player.playAnim("VENDING", "VEND_Drink2_P", { ms: 3330 });  // snacks: VEND_Eat_P, 1670
player.health = Math.min(100, player.health + 30);
player.frozen = false;