Skip to content

Room and events

Events

Callbacks the room calls when players act, matches change and the room reports problems.

Assign a function to an event property to handle it; assign null to stop. Each event has one handler. Handlers run with the room as this; a handler that throws, or returns a rejected promise, reports to onError as <event>: <error> and the room keeps running. After the room closes, no events fire except a final onRecordingComplete.

Many events receive byPlayer: the admin who made the change from the game, or null when your code or the room itself made it.

Not exported by name; reach it through the values that return it.

Room

event
room.onRoomLink = (url) => void

Fires once, just after createRoom() resolves. Assign it right after createRoom() returns; it is not replayed for a handler assigned later.

Arguments

urlstringrequired
The invitation link, the same as roomLink.
JavaScript
const room = await createRoom(config);
room.onRoomLink = (url) =>
  console.log('Room open at', url);

onError

event
room.onError = (message) => void

Reports problems the room handles without stopping your process:

  • A handler that threw: onPlayerJoin: TypeError: …
  • A command that rejected, such as Error: Invalid team. The command's promise rejects too.
  • Why the room ended, such as API key revoked or Host connection ended.
  • Network problems, such as Could not connect directly to this room. Try another network or room.
  • Host scheduler stalled; match paused. when your process was blocked for more than half a second.

Arguments

messagestringrequired
What went wrong, as text.
JavaScript
room.onError = (message) =>
  console.error(
    `[room ${room.roomId}]`,
    message,
  );

Players

onPlayerJoin

event
room.onPlayerJoin = (p) => void

Fires when a player has joined and can play. It does not fire for the host player.

Arguments

pHostPlayerrequired
The player who joined.
JavaScript
room.onPlayerJoin = (player) => {
  void room.sendAnnouncement(
    `Welcome, ${player.name}`,
    player.id,
  );
};

onPlayerLeave

event
room.onPlayerLeave = (p) => void

Fires whenever a player leaves, including when they are kicked or banned.

Arguments

pHostPlayerrequired
The player who left, with position set to null.

onPlayerKicked

event
room.onPlayerKicked = (p, reason, ban, byPlayer) => void

Fires after onPlayerLeave for a kick or ban.

Arguments

pHostPlayerrequired
The player who was removed.
reasonstringrequired
The reason shown to them.
banbooleanrequired
true if they were also banned.
byPlayerHostPlayer | nullrequired
The admin who removed them, or null.
JavaScript
room.onPlayerKicked = (
  player,
  reason,
  ban,
  by,
) => {
  log(
    `${player.name}` +
      ` ${ban ? 'banned' : 'kicked'}` +
      ` by ${by?.name ?? 'host'}: ${reason}`,
  );
};

onPlayerTeamChange

event
room.onPlayerTeamChange = (p, byPlayer) => void

Fires when a player's team actually changes.

Arguments

pHostPlayerrequired
The player, on their new team.
byPlayerHostPlayer | nullrequired
The admin who moved them, or null.

onPlayerAdminChange

event
room.onPlayerAdminChange = (p, byPlayer) => void

Arguments

pHostPlayerrequired
The player, with their new admin value.
byPlayerHostPlayer | nullrequired
Always null: only your code grants admin.

onPlayerMuteChange

event
room.onPlayerMuteChange = (p, byPlayer) => void

Arguments

pHostPlayerrequired
The player, with their new muted value.
byPlayerHostPlayer | nullrequired
The admin who changed it, or null.

onPlayerActivity

event
room.onPlayerActivity = (p) => void

Fires when a player changes their keys or sends a room message. Use it to track idle players.

Arguments

pHostPlayerrequired
The active player.
JavaScript
const lastSeen = new Map();
room.onPlayerActivity = (player) =>
  lastSeen.set(player.id, Date.now());

onPlayerInput

event
room.onPlayerInput = (p, prevInput) => void

Runs only when a peer changes its accepted key state; prevInput is the previous authoritative bitmask.

Fires each time a player's accepted key state changes. Keys are a bitmask: up 1, down 2, left 4, right 8, kick 16. Keys that are released because a player went silent for 250 ms don't fire this event.

Arguments

pHostPlayerrequired
The player, whose input is the new key state.
prevInputnumberrequired
The key state before this change.
JavaScript
const KICK = 16;
room.onPlayerInput = (player, prev) => {
  if (player.input & KICK && !(prev & KICK))
    kicksPressed++;
};

Chat

onPlayerChat

event
room.onPlayerChat = (p, text) => boolean | void

Fires for each room message from a player, before anyone sees it. Messages from muted players and players over the chat rate limit never arrive here. Only a synchronous false suppresses the message: an async handler returns a promise, which does not.

Arguments

pHostPlayerrequired
The sender.
textstringrequired
The message, 1–200 characters.

Return value

boolean | voidReturn false to stop the message from reaching the room.

JavaScript
room.onPlayerChat = (player, message) => {
  if (message.startsWith('!')) {
    handleCommand(player, message);
    // Keep commands out of the chat.
    return false;
  }
};

onPlayerDirectChat

event
room.onPlayerDirectChat = (p, recipient, text) => boolean | void

Host-visible direct chat; return false to suppress delivery to both participants.

Fires for each direct message between players, including messages to the host player. The host routes direct messages, so your room can read them. Returning false, returning a promise or throwing all stop delivery.

Arguments

pHostPlayerrequired
The sender.
recipientHostPlayerrequired
The player it is addressed to.
textstringrequired
The message.

Return value

boolean | voidReturn false to deliver it to neither player.

JavaScript
room.onPlayerDirectChat = (
  player,
  recipient,
  message,
) => !containsLink(message);

Matches

onGameStart

event
room.onGameStart = (byPlayer) => void

Arguments

byPlayerHostPlayer | nullrequired
The admin who started it, or null.

onGameStop

event
room.onGameStop = (byPlayer) => void

Fires when a match is stopped, and with null when a finished match returns to the lobby on its own.

Arguments

byPlayerHostPlayer | nullrequired
The admin who stopped it, or null.

onGamePause

event
room.onGamePause = (byPlayer) => void

Arguments

byPlayerHostPlayer | nullrequired
The admin who paused, or null.

onGameUnpause

event
room.onGameUnpause = (byPlayer) => void

Fires when the resume countdown begins.

Arguments

byPlayerHostPlayer | nullrequired
The admin who resumed, or null.

onGamePauseChange

event
room.onGamePauseChange = (paused) => void

Fires after onGamePause or onGameUnpause, for handlers that only need the state.

Arguments

pausedbooleanrequired
The new state.

onTeamGoal

event
room.onTeamGoal = (team) => void

Arguments

team1 | 2required
The team that scored: 1 red or 2 blue.
JavaScript
room.onTeamGoal = (team) => {
  const { red, blue } = room.getScores();
  void room.sendAnnouncement(
    `${team === 1 ? 'Red' : 'Blue'} scores!` +
      ` ${red}–${blue}`,
  );
};

onPositionsReset

event
room.onPositionsReset = () => void

Fires when players return to kickoff positions after a goal.

onTeamVictory

event
room.onTeamVictory = (scores) => void

Fires when a team wins and the match enters the finished phase.

Arguments

scoresHostScoresrequired
The final score.
JavaScript
room.onTeamVictory = ({ red, blue }) => {
  void room.sendAnnouncement(
    `Full time: ${red}–${blue}`,
    null,
    null,
    'bold',
  );
};

onGameVictory

event
room.onGameVictory = (scores) => void

Legacy Ball2D callback; prefer onTeamVictory.

The earlier name for onTeamVictory, which fires first. Use onTeamVictory in new code.

Arguments

scoresHostScoresrequired
The final score.

onGameTick

event
room.onGameTick = () => void

Fires 60 times a second during a match, before each simulation step, including the goal and finished phases. It does not fire in the lobby, while paused or during the resume countdown. Keep it fast: it runs on the same thread as the simulation.

JavaScript
room.onGameTick = () => {
  const ball = room.getBallPosition();
  if (ball) heatmap.add(ball.x, ball.y);
};

onPlayerBallKick

event
room.onPlayerBallKick = (p) => void

Arguments

pHostPlayerrequired
The player whose kick hit the ball.

Settings

onStadiumChange

event
room.onStadiumChange = (name, byPlayer) => void

Arguments

namestringrequired
The new stadium’s name.
byPlayerHostPlayer | nullrequired
The admin who changed it, or null.

onTeamsLockChange

event
room.onTeamsLockChange = (locked, byPlayer) => void

Arguments

lockedbooleanrequired
The new lock state.
byPlayerHostPlayer | nullrequired
The admin who changed it, or null.

onKickRateLimitSet

event
room.onKickRateLimitSet = (min, rate, burst, byPlayer) => void

Reports the limits after clamping. See setKickRateLimit().

Arguments

minnumberrequired
Cooldown in ticks.
ratenumberrequired
Recharge cost per kick in ticks.
burstnumberrequired
Kicks that can be saved up.
byPlayerHostPlayer | nullrequired
The admin who changed it, or null.

Recording

onRecordingComplete

event
room.onRecordingComplete = (blob, reason) => void

Delivers a recording the room finished on its own. Recordings you stop with stopRecording() are returned there instead. This is the one event that can fire while the room closes.

Arguments

blobBlobrequired
The recording.
reasonstringrequired
Recording limit reached, Stadium changed or Room closed.
JavaScript
room.onRecordingComplete = async (
  blob,
  reason,
) => {
  await writeFile(
    `auto-${Date.now()}.ball2drep`,
    Buffer.from(await blob.arrayBuffer()),
  );
  console.log('Saved recording:', reason);
};