# PlayersHandle

> Talos next · Since 0.7

The local players on the couch (`meta.players`) and the entity each drives.

Seating and unseating raise `player-joined` (`{ player, device, pad? }`), `player-left`, `player-disconnected` and `player-reconnected` (`{ player }`) on the event bus. A behaviour on a player's entity reads its own `ctx.input` exactly as a single player's does.

## Signature

```ts
export interface PlayersHandle {
  /** Every seated player, in number order. */
  list(): LocalPlayer[]
  /** How many players are seated. */
  readonly count: number
  /** The scene's player limit, 1 to 4; 1 for a scene without a players block. */
  readonly max: number
  /** Whether free devices may join now. Open in a lobby, closed during play. */
  readonly joining: boolean
  /** Open or close joining: a lobby opens it, a race closes it. Seated players stay seated. */
  setJoining(open: boolean): void
  /** Seat a device from a script (`join: "script"` scenes). The new number, or null when no seat was free. */
  join(device: "keyboard" | "keyboard-left" | "keyboard-right"): number | null
  join(device: "pad", pad: number): number | null
  /** Seat a player with no device on this machine: a peer's, or one a test or tool drives by input frames. */
  join(device: "remote"): number | null
  /** Unseat a player; their devices are free to join again. */
  leave(number: number): boolean
  /** Drive `entity` with player `number`'s devices; null goes back to the scene's `spawn { for: "player", player }`. */
  bind(number: number, entity: EntityHandle | string | null): boolean
  /** The player whose devices drive `entity`, or 0. */
  of(entity: EntityHandle | string): number
  /** Player `number` (2 and up) reads its own input context; null follows player 1's again. */
  setContext(number: number, name: string | null): boolean
  /** The input context player `number` reads now, or null. */
  context(number: number): string | null
}
```

Import: `import type { PlayersHandle } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `list` | `list(): LocalPlayer[]` | Every seated player, in number order. |
| `count` | `readonly count: number` | How many players are seated. |
| `max` | `readonly max: number` | The scene's player limit, 1 to 4; 1 for a scene without a players block. |
| `joining` | `readonly joining: boolean` | Whether free devices may join now. Open in a lobby, closed during play. |
| `setJoining` | `setJoining(open: boolean): void` | Open or close joining: a lobby opens it, a race closes it. Seated players stay seated. |
| `join` | `join(device: "keyboard" \| "keyboard-left" \| "keyboard-right"): number \| null` | Seat a device from a script (`join: "script"` scenes). The new number, or null when no seat was free. |
| `leave` | `leave(number: number): boolean` | Unseat a player; their devices are free to join again. |
| `bind` | `bind(number: number, entity: EntityHandle \| string \| null): boolean` | Drive `entity` with player `number`'s devices; null goes back to the scene's `spawn { for: "player", player }`. |
| `of` | `of(entity: EntityHandle \| string): number` | The player whose devices drive `entity`, or 0. |
| `setContext` | `setContext(number: number, name: string \| null): boolean` | Player `number` (2 and up) reads its own input context; null follows player 1's again. |
| `context` | `context(number: number): string \| null` | The input context player `number` reads now, or null. |

## Example

The tested fixture `fixtures/recipes/collectible-and-goal/scenes/behaviors/collectible.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A coin the player collects by walking into it.
 *
 * It says so once, as `collected`, and leaves the world. Counting is somebody else's job —
 * `goal-exit.ts` — so a coin never needs to know how many there are.
 *
 * The contact is `onTrigger`, which needs three things in the scene: a collider with
 * `isTrigger: true` (the volume), a `trigger` component (what fires), and a body on the player —
 * the character controller's capsule is one.
 */
interface State {
  collected: boolean
}

interface Props {
  /** Tag the toucher must carry. Default `player`. */
  playerTag?: string
}

export default {
  init(ctx) {
    if (typeof ctx.state.collected !== "boolean") ctx.state.collected = false
  },

  onTrigger(ctx, trigger) {
    if (ctx.state.collected || !trigger.entering) return
    if (!trigger.other.hasTag(ctx.props.playerTag ?? "player")) return
    ctx.state.collected = true
    // The `goal` component on this coin has the same id, so the playtester and the HUD agree.
    ctx.events.emit("collected", { goal: ctx.entity.id })
    // Last: after `despawn` every member of this entity's handles throws.
    ctx.entity.despawn()
  },
} satisfies Behavior<State, Props>
```
