# Who sees what

> Send chat, sounds, Menu parts, drawings, discs and walls to some players only, so a game can keep secrets.

Every message, sound, Menu or HUD part, drawing, disc and wall can name its
**audience**: who receives it. The room checks the audience for each player before sending
anything, so a player outside it never gets the data, not even hidden.

That is what lets a game keep secrets: a hint only one player sees, a
whisper, footsteps only nearby players hear.

## Audiences

| Audience                              | Who receives it                                                                           |
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
| none (the default)                    | everyone, spectators and replays too                                                      |
| `{ include: [player] }`               | the listed players                                                                        |
| `{ exclude: [player] }`               | everyone except them                                                                      |
| `{ role }`                            | players holding a [role](/games/roles)                                                    |
| `{ near: 150, blockedBy: ["walls"] }` | players whose disc is within 150 units, with no wall of those collision groups in between |
| `{ any: [ … ] }`                      | anyone in at least one of them                                                            |

`role`, `near` and `any` also take `exclude`, and exclusions always win:
`{ role: referee, exclude: [player] }` is every referee but one.

`near` only works for things with a position the room knows: a sound with
a `source`, a disc, a wall, or a [drawing](#drawings) on one of them or at a
fixed point.

## Chat

```ts title="src/main.ts"
// Only the new player reads this.
game.on(Events.PlayerJoin, ({ player }) => {
  game.send("Find the others before time runs out.", {
    audience: { include: [player] },
  });
});
```

## Sounds

```ts title="src/main.ts"
let ball: Disko.Disc | null = null;

game.on(Events.Start, () => {
  ball = world.createDisc({ radius: 8 });
  // Everyone hears the whistle.
  game.sound(whistle);
});

// Only players within 150 units of the ball hear it, and not through walls.
game.on(Events.Tick, ({ tick }) => {
  if (ball !== null && tick % 30 === 0) {
    game.sound(footsteps, {
      source: ball,
      audience: { near: 150, blockedBy: ["walls"] },
    });
  }
});
```

A sound plays when the player's screen reaches the moment it happened,
never twice, and never for a player who joins later.

`volume` is a number from 0 to 1, or an expression each listener's screen
works out on every frame while the sound plays. Fade a sound with distance
like this:

```ts title="src/main.ts"
// Each listener's screen works out the volume while the sound plays: full
// within 40 units of the ball, fading out until silent at 300.
game.on(Events.Tick, ({ tick }) => {
  if (ball !== null && tick % 60 === 0) {
    const away = expr.distance(expr.me.position, expr.disc(ball).position);

    game.sound(whistle, {
      source: ball,
      volume: expr.smoothstep(300, 40, away),
    });
  }
});
```

A volume that works out to nothing is silent.

## Menu and HUD

A part outside its audience is left out, with everything inside it. With
`outside: Outside.Disabled` it is shown but can't be used:

```ts title="src/main.ts"
ui.menu(
  ui.stack({
    children: [
      ui.text({ text: "Hide and seek" }),
      // Only referees receive this; nobody else learns it exists.
      ui.text({
        text: "You are refereeing.",
        audience: { role: referee },
      }),
      // Everyone sees this button, but only referees can press it.
      ui.button({
        label: "Blow the whistle",
        audience: { role: referee },
        outside: Outside.Disabled,
        on: { activate: ui.request({ event: "whistle", requires: referee }) },
      }),
    ],
  }),
);
```

The audience of a Menu or HUD part is checked again whenever a player's
roles change.

## Hidden discs and walls

A disc or wall with an audience reaches only the players in it. Everyone
else never receives it: not its place, not its moves, not that it exists.
A `near` audience is checked every tick, so things come and go as players
move:

```ts title="src/main.ts"
// Hide and seek: another player's disc reaches you only when your own disc
// is near it, with no wall of the "walls" group in between.
game.on("player-enter", ({ player }) => {
  const disc = world.createDisc({
    x: 50 * player.id,
    radius: 12,
    audience: { near: 150, blockedBy: ["walls"] },
  });

  player.control(disc);
});

game.on("start", () => {
  const hedge = world.createWall({
    points: [
      { x: 0, y: -80 },
      { x: 0, y: 80 },
    ],
    groups: ["walls"],
  });

  world.draw.line({
    anchor: hedge,
    points: [
      { x: 0, y: -80 },
      { x: 0, y: 80 },
    ],
    stroke: rgb(40, 120, 60),
    strokeWidth: 6,
  });
});
```

Hidden things still push what players can see. A player sees only the
push: the ball they can see changes direction, with no hint of what hit it.
A player always knows the disc they control, and a drawing on a hidden
disc or wall is hidden with it. `setAudience` changes an audience at any
time.

Replays show everything: they are for after the match.

## Drawings

A drawing with a `near` audience reaches the players near it, checked every
tick. The room measures from the disc or wall the drawing follows, or from
its fixed point:

```ts title="src/main.ts"
// A ring that follows the ball and a sign at a fixed point, each sent only
// to players within 200 units of it.
game.on(Events.Start, () => {
  if (ball !== null) {
    world.draw.circle({
      anchor: ball,
      radius: 14,
      stroke: rgb(255, 255, 255),
      audience: { near: 200 },
    });
  }

  world.draw.text({
    text: "Treasure",
    size: 12,
    position: { x: 400, y: 0 },
    fill: rgb(250, 210, 80),
    audience: { near: 200 },
  });
});
```

The room must know where the drawing is without asking screens. A drawing
placed by an expression, such as one that follows the viewer's own disc
with `expr.me.position`, can't be `near`: give it an `anchor` instead. A
player standing at the edge keeps a drawing a little farther, so it
doesn't flicker as they move.

### Fading in

A `near` drawing arrives whole. To fade it in, give it an `opacity` that
grows as the viewer comes closer, with the audience a little wider than
where the fade starts:

```ts title="src/main.ts"
// The chest arrives at 200 units, invisible, and is fully drawn by 120.
const chest = { x: -400, y: 0 };

game.on(Events.Start, () => {
  const away = expr.distance(expr.me.position, chest);

  world.draw.circle({
    position: chest,
    radius: 10,
    fill: rgb(200, 160, 60),
    opacity: expr.smoothstep(200, 120, away),
    audience: { near: 200 },
  });
});
```

In Studio, set the visual's **Opacity** to **Fade by distance**, with
**From** the visual's place, and its **Shown to** to **Players near** it.
