# Players, teams & actions

> React to players joining and entering matches, give them discs, declare actions and permissions.

This page covers the people in your game: how they come and go, how they
control their discs, what buttons they can press, and how a room can limit
what each of them may do.

## In the room, or in the match

A player can be in the room without playing. They might be watching, picking
a team, or waiting for the next match. So disko keeps two separate ideas:

- **Connected:** the player is in the room. `player-join` and
  `player-leave` mark this.
- **Participating:** the player is in the running match and can control a
  disc. `player-enter` and `player-exit` mark this.

Everyone who is connected but not participating is a **spectator**.

```mermaid alt="A player joins the room as a spectator. player.play() makes them a participant in the match (player-enter); player.spectate() or the match ending makes them a spectator again (player-exit). Leaving the room fires player-leave."
stateDiagram-v2
  [*] --> Spectator: player-join
  Spectator --> Participant: play() → player-enter
  Participant --> Spectator: spectate() → player-exit
  Spectator --> [*]: player-leave
  Participant --> [*]: player-leave
```

`player.play()` asks to make a player a participant, and `player.spectate()`
moves them back. `game.players` lists every connected player, spectators
included. You always get the same `Disko.Player` object for the same player,
and `player.id` stays the same for as long as they're connected.

| Event          | When                                       |
| -------------- | ------------------------------------------ |
| `player-join`  | a player connected to the room             |
| `player-leave` | a player left                              |
| `player-enter` | a player became a participant of the match |
| `player-exit`  | a player went back to spectating           |

## Teams are yours

disko doesn't have teams built in, because every game wants something
different. Keep teams in your own data, for example a `Map` from `player.id`
to a team, as the template does.

## Discs and movement

To let a player move, give them a disc. `player.control(disc)` connects the
player's movement keys to that disc, and `player.release()` disconnects them.
disko applies the movement itself, smoothly, on every screen.
`setAcceleration` and `setDamping` tune how quick and how slippery it feels.

This assigns each new player a team and gives them a disc on their team's
side when they enter the match:

```ts title="src/players.ts"
type Team = "red" | "blue";
const teams = new Map<number, Team>();

game.on("player-join", ({ player }) => {
  teams.set(player.id, teams.size % 2 === 0 ? "red" : "blue");
  game.send(`${player.name} joined`);
});

game.on("player-leave", ({ player }) => {
  teams.delete(player.id);
});

game.on("player-enter", ({ player }) => {
  const side = teams.get(player.id) === "red" ? -1 : 1;
  const disc = world.createDisc({ x: side * 100, y: 0, radius: 15 });

  player.control(disc);
  player.useActionSchema(controls);
});
```

## Actions

Movement isn't the only input. An **action** is anything else a player can
do with a key, such as kicking.

You group actions into an **action schema** and give a schema to each player
with `player.useActionSchema`. Different players can have different
schemas: a goalkeeper might have a dive that others don't.

Schemas are created in top-level code. An action can have a **native
behavior**, such as `kick`, that disko already knows how to perform. The
player's browser can then show the kick instantly, before the room confirms
it, so it feels responsive even on a slow connection.

```ts title="src/players.ts"
// Action schemas are created during top-level evaluation.
const controls = game.createActionSchema();
const kick = controls.defineAction({
  label: "Kick",
  behavior: { type: "kick", parameters: { reach: 1.6, strength: 2 } },
});
```

Four events let you follow and shape actions:

- `action-press` and `action-release` report the key going down and up.
- `before-action` runs before a native behavior and can `cancel()` it.
- `action` reports what happened, including which discs a kick hit.

```ts title="src/players.ts"
game.on("before-action", (event) => {
  // Cancel kicks by players without a disc.
  if (event.action === kick && event.controlledObject === null) {
    event.cancel();
  }
});

game.on("action", ({ player, action, targets }) => {
  if (action === kick && targets.length > 0) {
    game.send({
      content: [{ text: player.name, weight: "bold" }, { text: " kicked" }],
      sound: "none",
    });
  }
});
```

## Chat and room events

`game.send` posts a message in the room chat, as plain text or styled spans.

`game.emit(name, payload)` tells the room about something in your game, such
as a goal. The room receives it as the `game:<name>` event and can react, for
example by keeping statistics. See
[Events, chat & lifecycle](/rooms/events-chat-lifecycle).

## Permissions

Many games have actions only some players should take: starting the match,
picking teams, changing settings. But who should be allowed depends on the
room, not the game. One room has admins, another lets everyone do
everything.

So the work is split:

- **Your game declares** what can be restricted, with a sensible default.
- **The room decides** who holds each permission, using its own idea of
  roles.

```mermaid alt="The game declares a permission, for example pick-teams with the default restricted. The room may decide who holds it, for example its admins. When the game asks game.can(player, pickTeams), the room's decision wins; if the room decided nothing, the game's default applies."
flowchart TB
  declare["Game declares<br/>pick-teams, default: restricted"] --> can{"game.can(player, pickTeams)"}
  room["Room decides<br/>e.g. admins hold it"] --> can
  can -- "room decided" --> answer1["Room's decision"]
  can -- "room decided nothing" --> answer2["Game's default"]
```

Your game never needs to know what an "admin" is, and it works in a room
that decides nothing at all.

### Declaring permissions

Declare permissions in top-level code with
`game.permission(name, { default, label })`, up to 64 per game. The default
is who holds it when the room decides nothing:

| Default        | Who holds it                                                         |
| -------------- | -------------------------------------------------------------------- |
| `"everyone"`   | every player: a normal player ability                                |
| `"restricted"` | no normal player; only the room grants it, for example to its admins |
| `"owner"`      | the player signed in with the room owner's account, if connected     |

```ts title="src/permissions.ts"
// Permissions are declared during top-level evaluation, with defaults that
// work in a room that decides nothing.
const manageMatch = game.permission("manage-match", {
  default: "everyone",
  label: "Start and stop the match",
});
const pickTeams = game.permission("pick-teams", {
  default: "restricted",
  label: "Pick teams",
});
```

### Using them in the Menu and HUD

The simplest use is to gate parts of the UI with `requires`:

- On a request, `ui.request({ …, requires })`: players without the
  permission see the control disabled and can't send the request. The room
  checks this before the request ever reaches your game.
- On a node, `requires: permission` hides the node and everything inside it
  from players without the permission. `requires: { permission, denied: "disable" }`
  shows it disabled instead.

`ui.playerName({ player })` shows a player's name in the room's color with
its badge, so players can see who's an admin:

```ts title="src/permissions.ts"
function rosterRow(player: Disko.Player): Disko.UiNode {
  return ui.row({
    key: `player-${String(player.id)}`,
    children: [
      ui.playerName({ player }),
      ui.button({
        label: "Move to play",
        // Players without `pick-teams` see the button disabled.
        requires: { permission: pickTeams, denied: "disable" },
        on: {
          activate: ui.request({
            event: `play:${String(player.id)}`,
            requires: pickTeams,
          }),
        },
      }),
    ],
  });
}

// A slot needs at least one child.
function rosterRows(): Disko.UiNode[] {
  return game.players.length === 0
    ? [ui.text({ text: "No players yet" })]
    : game.players.map(rosterRow);
}

const roster = ui.slot({ key: "roster", children: rosterRows() });
const captainNote = ui.slot({
  key: "captain",
  children: [ui.text({ text: "Waiting for teams" })],
});

ui.menu(
  ui.stack({
    children: [
      // Hidden from players without `manage-match` (the default `denied`).
      ui.button({
        label: "Start",
        requires: manageMatch,
        on: { activate: ui.request({ event: "start", requires: manageMatch }) },
      }),
      captainNote,
      roster,
    ],
  }),
);
```

### Checking in code

`game.can(player, permission)` answers immediately. Within one handler,
every call sees the same answers, so your logic stays consistent. If the
room hasn't answered yet, `game.can` says no, which is the safe choice.

A game can also change its own default for one player with
`permission.setDefault(player, allowed)`, for example to make the first
player a captain. The room's decision still wins. The change applies after
the current handler finishes, and `permission-change` reports every change
to what `game.can` returns:

```ts title="src/permissions.ts"
function refreshRoster(): void {
  ui.push(
    ui.swap({ target: roster, method: "children", content: rosterRows() }),
  );
}

game.on("player-join", ({ player }) => {
  // The first player to join picks teams unless the room decides otherwise.
  if (game.players.length === 1) {
    pickTeams.setDefault(player, true);
  }

  refreshRoster();
});

game.on("player-leave", refreshRoster);

game.on("ui-request", (request) => {
  if (request.event === "start") {
    game.start();
  } else if (request.event.startsWith("play:")) {
    // The room already checked `requires` before the request arrived;
    // game.can reads the same decisions for the rest of this turn.
    const target = game.players.find(
      (player) => `play:${String(player.id)}` === request.event,
    );

    if (target !== undefined && game.can(request.player, pickTeams)) {
      target.play();
    }
  }
});

game.on("permission-change", ({ player, permission, allowed }) => {
  if (permission === pickTeams) {
    ui.push(
      player,
      ui.swap({
        target: captainNote,
        method: "children",
        content: ui.text({
          text: allowed ? "You pick the teams" : "Waiting for teams",
        }),
      }),
    );
  }
});
```

Native actions such as kicks aren't permission-checked: to restrict them,
give a player a different action schema. For the room's side of
permissions, see
[Permissions & moderation](/rooms/permissions-and-moderation).
