A minimal, fully working starter for building co-op (players-vs-the-game) experiences on Plot with an authoritative server.
The server is the star: it owns the enemies, their movement, all damage resolution, and the shared base. Clients do almost nothing — they send WASD movement intents and render the world the server snapshots each tick. This is the foundation for co-op survival / PvE games (wave shooters, tower defense, horde survival) where you never want the world's truth to live on a client.
It is intentionally lean. Everything in it works; nothing is stubbed.
src/logic.ts Pure game math — no @plot imports, unit-testable
src/handler.ts The authoritative room (onJoin/onMessage/onTick/onLeave)
src/main.ts Canvas client: predict self, interpolate others + enemies
src/logic.test.ts Vitest tests for the pure logic
vendor/ Vendored @plot/client and @plot/handler
npm install
npm run devThen open two browser tabs at the dev URL — both join room COOP1, so you
will see each other's player and the same server-driven enemies. Move with
WASD. Stand near an enemy to auto-damage it; let one reach the base and the
base HP drops.
You need either:
- a
VITE_PLOT_APP_KEYfrom plot.ws (hosted), or - a local Plot server via
VITE_PLOT_API_URL(e.g.http://localhost:8787).
Set them in a .env.local file:
VITE_PLOT_APP_KEY=your_app_key
# or, for local development:
VITE_PLOT_API_URL=http://localhost:8787
- Authoritative
onTicksimulation.handler.tsadvances the world 20 times a second via the purestepWorldinlogic.ts: it spawns enemies on a tick counter (never wall-clock time, so it stays deterministic), walks them toward the nearest player or the base, applies player auto-fire, and removes enemies that die or reach the base. - Client prediction. The client
predicts only its ownplayers.<id>.pos, so local movement feels instant and is reconciled against the server. - Interpolation. Other players (
players.*.pos) and all enemies (enemies.*.pos) are interpolated for smooth motion between server snapshots. - Snapshot rendering.
baseHpandscoreare read straight fromroom.currentState— the authoritative truth.
- Waves & a director: spawn in batches, gate on a clear, escalate difficulty.
- Weapons & abilities: add input kinds beyond
moveand resolve them inonMessage/onTick. - Win / lose conditions: end the round when
baseHp <= 0or a wave goal is met, andbroadcasta game-over event. - More enemy types: give enemies a
kind, with different speed/HP/damage. - Leaderboards & persistence: the handler context exposes
leaderboard,save, andprofile.
@plot/client and @plot/handler are vendored under vendor/ and referenced
as file: dependencies so the starter runs out of the box. Once the packages
are published, replace the file: entries in package.json with normal npm
versions:
npm i @plot/client @plot/handlerBuild the client with npm run build (output in dist/) and host it as static
files anywhere (any static host / CDN). The authoritative room handler in
src/handler.ts runs on Plot's infrastructure — deploy it through your Plot
app. Point the client at your app via VITE_PLOT_APP_KEY.
MIT — see LICENSE.
