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
onRoomLink
room.onRoomLink = (url) => voidFires once, just after createRoom() resolves. Assign it right after createRoom() returns; it is not replayed for a handler assigned later.
Arguments
stringrequiredconst room = await createRoom(config);
room.onRoomLink = (url) =>
console.log('Room open at', url);onError
room.onError = (message) => voidReports 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 revokedorHost 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
stringrequiredroom.onError = (message) =>
console.error(
`[room ${room.roomId}]`,
message,
);Players
onPlayerJoin
room.onPlayerJoin = (p) => voidFires when a player has joined and can play. It does not fire for the host player.
Arguments
room.onPlayerJoin = (player) => {
void room.sendAnnouncement(
`Welcome, ${player.name}`,
player.id,
);
};onPlayerLeave
room.onPlayerLeave = (p) => voidFires whenever a player leaves, including when they are kicked or banned.
Arguments
position set to null.onPlayerKicked
room.onPlayerKicked = (p, reason, ban, byPlayer) => voidFires after onPlayerLeave for a kick or ban.
Arguments
stringrequiredbooleanrequiredtrue if they were also banned.null.room.onPlayerKicked = (
player,
reason,
ban,
by,
) => {
log(
`${player.name}` +
` ${ban ? 'banned' : 'kicked'}` +
` by ${by?.name ?? 'host'}: ${reason}`,
);
};onPlayerTeamChange
room.onPlayerTeamChange = (p, byPlayer) => voidFires when a player's team actually changes.
Arguments
null.onPlayerAdminChange
room.onPlayerAdminChange = (p, byPlayer) => voidArguments
admin value.null: only your code grants admin.onPlayerMuteChange
room.onPlayerMuteChange = (p, byPlayer) => voidArguments
muted value.null.onPlayerActivity
room.onPlayerActivity = (p) => voidFires when a player changes their keys or sends a room message. Use it to track idle players.
Arguments
const lastSeen = new Map();
room.onPlayerActivity = (player) =>
lastSeen.set(player.id, Date.now());onPlayerInput
room.onPlayerInput = (p, prevInput) => voidRuns 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
input is the new key state.numberrequiredconst KICK = 16;
room.onPlayerInput = (player, prev) => {
if (player.input & KICK && !(prev & KICK))
kicksPressed++;
};Chat
onPlayerChat
room.onPlayerChat = (p, text) => boolean | voidFires 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
stringrequiredReturn value
boolean | voidReturn false to stop the message from reaching the room.
room.onPlayerChat = (player, message) => {
if (message.startsWith('!')) {
handleCommand(player, message);
// Keep commands out of the chat.
return false;
}
};onPlayerDirectChat
room.onPlayerDirectChat = (p, recipient, text) => boolean | voidHost-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
stringrequiredReturn value
boolean | voidReturn false to deliver it to neither player.
room.onPlayerDirectChat = (
player,
recipient,
message,
) => !containsLink(message);Matches
onGameStart
room.onGameStart = (byPlayer) => voidArguments
null.onGameStop
room.onGameStop = (byPlayer) => voidFires when a match is stopped, and with null when a finished match returns to the lobby on its own.
Arguments
null.onGamePause
room.onGamePause = (byPlayer) => voidArguments
null.onGameUnpause
room.onGameUnpause = (byPlayer) => voidFires when the resume countdown begins.
Arguments
null.onGamePauseChange
room.onGamePauseChange = (paused) => voidFires after onGamePause or onGameUnpause, for handlers that only need the state.
Arguments
booleanrequiredonTeamGoal
room.onTeamGoal = (team) => voidArguments
1 | 2required1 red or 2 blue.room.onTeamGoal = (team) => {
const { red, blue } = room.getScores();
void room.sendAnnouncement(
`${team === 1 ? 'Red' : 'Blue'} scores!` +
` ${red}–${blue}`,
);
};onPositionsReset
room.onPositionsReset = () => voidFires when players return to kickoff positions after a goal.
onTeamVictory
room.onTeamVictory = (scores) => voidFires when a team wins and the match enters the finished phase.
Arguments
room.onTeamVictory = ({ red, blue }) => {
void room.sendAnnouncement(
`Full time: ${red}–${blue}`,
null,
null,
'bold',
);
};onGameVictory
room.onGameVictory = (scores) => voidLegacy Ball2D callback; prefer onTeamVictory.
The earlier name for onTeamVictory, which fires first. Use onTeamVictory in new code.
Arguments
onGameTick
room.onGameTick = () => voidFires 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.
room.onGameTick = () => {
const ball = room.getBallPosition();
if (ball) heatmap.add(ball.x, ball.y);
};onPlayerBallKick
room.onPlayerBallKick = (p) => voidArguments
Settings
onStadiumChange
room.onStadiumChange = (name, byPlayer) => voidArguments
stringrequirednull.onTeamsLockChange
room.onTeamsLockChange = (locked, byPlayer) => voidArguments
booleanrequirednull.onKickRateLimitSet
room.onKickRateLimitSet = (min, rate, burst, byPlayer) => voidReports the limits after clamping. See setKickRateLimit().
Arguments
numberrequirednumberrequirednumberrequirednull.Recording
onRecordingComplete
room.onRecordingComplete = (blob, reason) => voidDelivers 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
BlobrequiredstringrequiredRecording limit reached, Stadium changed or Room closed.room.onRecordingComplete = async (
blob,
reason,
) => {
await writeFile(
`auto-${Date.now()}.ball2drep`,
Buffer.from(await blob.arrayBuffer()),
);
console.log('Saved recording:', reason);
};