Getting started

Create handles, register listeners, call ready() once, then read, write, and send throughout your game.

Install with npm

npm install @plot.game/sdk

Import the shared Plot object. Create any state views and register their listeners before calling ready():

import Plot from '@plot.game/sdk'

type GameState = {
  phase?: 'lobby' | 'playing' | 'complete'
}

const gameState = Plot.gameState.as<GameState>()

Plot.onLifecycleChange((status) => {
  setPaused(status === 'disconnected')
  if (status === 'connected') {
    renderPhase(gameState.get().phase ?? 'lobby')
  }
})

gameState.onChanged(({ current }) => {
  renderPhase(current.phase ?? 'lobby')
})

await Plot.ready()

const party = Plot.party.get()
console.log(`You are ${party.self.displayName}`)

That is the standard startup order:

  1. Create typed state views and channels.
  2. Register lifecycle, focused state, and channel listeners.
  3. await Plot.ready().
  4. Read state, write state, and send messages.

Calling ready() tells Plot that this game copy is ready to participate. Plot may already have admitted the player with a stable ID that other copies see as joining; calling ready() does not add or renumber them. The promise resolves only when:

  • Plot.party contains the complete current party and party.self is connected;
  • Game State and every player's Player Data are installed;
  • the synchronized clock is ready; and
  • state changes and realtime messages can be exchanged.

This is SDK readiness. It does not wait for your framework to render or for optional assets to load. Call it at the point where your game is prepared to participate.

ready() returns no client object. After it resolves, reads such as Plot.party.get(), Game State and Player Data getters, and Plot.time.now() return immediately from the latest local snapshot.

Before ready(), you may create typed or schema-backed state views, create channels, register any listener, and inspect Plot.connection. Those local setup calls do not start readiness. The connected lifecycle callback runs after all state is installed, just before ready() resolves. It runs again after recovery has installed current state, before retained channel messages. It does not run for ordinary data changes; focused change listeners handle those. Complete per-domain snapshot subscriptions remain available as an alternative for a view that follows just one domain.

Reads and channel sends made too early throw PlotNotReadyError; state writes return a rejected promise. Inside a connected lifecycle callback, readiness-dependent APIs are already available. Most games call ready() without options; the optional parentOrigin is only for Plot-provided preview and test tooling.

Pick the right data surface

After readiness, choose the API by how long the information matters:

NeedUse
Shared phase, progress, results, or deadlinesPlot.gameState. Everyone reads; only the host writes.
A role, clue, score, or choice owned by one playerPlot.playerData. Everyone reads; each player writes their own object.
A button press, effect, pointer, or other transient valuePlot.realtime.channels.
Players, the host, the current player, or presencePlot.party.
Shared timestamps and deadlinesPlot.time.

Game State and Player Data are synchronized session state, not high-frequency streams. While the session remains active, startup and recovery install their current synchronized values. They are not permanent account or saved-progress storage. Use channels for movement, pointer samples, effects, and button presses; keep state that belongs only to one screen in your own app.

If the host coordinates game rules, start that coordinator after Plot.ready() on every page load and derive its next action from current Game State and Player Data. See How Plot games work for reload and session lifetime behavior.

Handle startup failure

Opening a game URL directly is useful for standalone UI work, but Plot.ready() needs a game session launched by Plot. Catch PlotError when you can offer a useful retry or exit choice:

import Plot, { PlotError } from '@plot.game/sdk'

try {
  await Plot.ready()
  startGame()
} catch (error) {
  if (!(error instanceof PlotError)) throw error
  showStartupProblem(error.code)
}

Use error.code for program logic; message text is diagnostic and may change. See Error handling for retry guidance and optional Plot.connection status. Game code never creates a reconnect loop.

Use a browser script

A game without a package manager can load the browser build from a CDN. Pin the complete version so a published game never changes underneath you:

<script
  src="https://cdn.jsdelivr.net/npm/@plot.game/sdk@0.2.0/dist/plot-sdk.iife.js"
  integrity="sha384-RCv1vjQIh/OukVKo7XswThS+Ed8ZwSBAayAVPVvpsFuWYgNfMEBRxn7uonvMmKp7"
  crossorigin="anonymous"
></script>
<script>
  void (async () => {
    await Plot.ready()
    const player = Plot.party.get().self
    document.querySelector('#player-name').textContent = player.displayName
  })()
</script>

Every release includes dist/integrity.json; copy its SHA-384 value when updating the pinned version.

Next, learn about party and players, then choose Game State or Player Data for synchronized state and realtime channels for transient messages. Use synchronized time whenever screens must agree on a deadline.